diff --git a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart index 71f31b5..34125a0 100644 --- a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart +++ b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart @@ -2,4 +2,5 @@ // // SPDX-License-Identifier: BSD-3-Clause +/// library flutter_timeline_firebase; diff --git a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart index 954aaef..fcd9330 100644 --- a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart +++ b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart @@ -4,6 +4,8 @@ /// library flutter_timeline_view; +export 'src/config/timeline_options.dart'; +export 'src/config/timeline_translations.dart'; export 'src/screens/timeline_post_creation_screen.dart'; export 'src/screens/timeline_post_screen.dart'; export 'src/screens/timeline_screen.dart'; diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart new file mode 100644 index 0000000..8cd1f6d --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause +import 'package:flutter/material.dart'; +import 'package:flutter_image_picker/flutter_image_picker.dart'; +import 'package:flutter_timeline_view/src/config/timeline_translations.dart'; +import 'package:intl/intl.dart'; + +@immutable +class TimelineOptions { + const TimelineOptions({ + this.translations = const TimelineTranslations(), + this.imagePickerConfig = const ImagePickerConfig(), + this.imagePickerTheme = const ImagePickerTheme(), + this.dateformat, + this.buttonBuilder, + this.textInputBuilder, + }); + + /// The format to display the post time in + final DateFormat? dateformat; + + final TimelineTranslations translations; + + final ButtonBuilder? buttonBuilder; + + final TextInputBuilder? textInputBuilder; + + /// ImagePickerTheme can be used to change the UI of the + /// Image Picker Widget to change the text/icons to your liking. + final ImagePickerTheme imagePickerTheme; + + /// ImagePickerConfig can be used to define the + /// size and quality for the uploaded image. + final ImagePickerConfig imagePickerConfig; +} + +typedef ButtonBuilder = Widget Function( + BuildContext context, + VoidCallback onPressed, + String text, { + bool enabled, +}); + + +typedef TextInputBuilder = Widget Function( + TextEditingController controller, + Widget? suffixIcon, + String hintText, +); diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart new file mode 100644 index 0000000..25c04c0 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; + +@immutable +class TimelineTranslations { + const TimelineTranslations({ + this.title = 'Title', + this.content = 'Content', + this.contentDescription = 'What do you want to share?', + this.uploadImage = 'Upload image', + this.uploadImageDescription = 'Upload an image to your message (optional)', + this.allowComments = 'Are people allowed to comment?', + this.allowCommentsDescription = + 'Indicate whether people are allowed to respond', + this.checkPost = 'Check post overview', + this.deletePost = 'Delete post', + this.viewPost = 'View post', + this.likesTitle = 'Likes', + this.commentsTitle = 'Comments', + this.writeComment = 'Write your comment here...', + }); + + final String title; + final String content; + final String contentDescription; + final String uploadImage; + final String uploadImageDescription; + final String allowComments; + final String allowCommentsDescription; + final String checkPost; + + final String deletePost; + final String viewPost; + final String likesTitle; + final String commentsTitle; + final String writeComment; +} diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart index 37bd25e..dcd806a 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_creation_screen.dart @@ -1,8 +1,237 @@ -import 'package:flutter/material.dart'; +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause -class TimelinePostCreationScreen extends StatelessWidget { - const TimelinePostCreationScreen({super.key}); +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_image_picker/flutter_image_picker.dart'; +import 'package:flutter_timeline_view/src/config/timeline_options.dart'; +import 'package:flutter_timeline_view/src/widgets/dotted_container.dart'; + +class TimelinePostCreationScreen extends StatefulWidget { + const TimelinePostCreationScreen({ + required this.options, + this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16), + super.key, + }); + + /// The options for the timeline + final TimelineOptions options; + + /// The padding around the screen + final EdgeInsets padding; @override - Widget build(BuildContext context) => const Placeholder(); + State createState() => + _TimelinePostCreationScreenState(); +} + +class _TimelinePostCreationScreenState + extends State { + TextEditingController titleController = TextEditingController(); + TextEditingController contentController = TextEditingController(); + Uint8List? image; + bool editingDone = false; + bool allowComments = false; + + @override + void initState() { + super.initState(); + titleController.addListener(checkIfEditingDone); + contentController.addListener(checkIfEditingDone); + } + + @override + void dispose() { + titleController.dispose(); + contentController.dispose(); + super.dispose(); + } + + void checkIfEditingDone() { + setState(() { + editingDone = titleController.text.isNotEmpty && + contentController.text.isNotEmpty && + image != null; + }); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Padding( + padding: widget.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.options.translations.title, + style: theme.textTheme.displaySmall, + ), + widget.options.textInputBuilder?.call( + titleController, + null, + '', + ) ?? + TextField( + controller: titleController, + ), + const SizedBox(height: 16), + Text( + widget.options.translations.content, + style: theme.textTheme.displaySmall, + ), + const SizedBox(height: 4), + Text( + widget.options.translations.contentDescription, + style: theme.textTheme.bodyMedium, + ), + // input field for the content + SizedBox( + height: 100, + child: TextField( + controller: contentController, + expands: true, + maxLines: null, + minLines: null, + ), + ), + const SizedBox( + height: 16, + ), + // input field for the content + Text( + widget.options.translations.uploadImage, + style: theme.textTheme.displaySmall, + ), + Text( + widget.options.translations.uploadImageDescription, + style: theme.textTheme.bodyMedium, + ), + // image picker field + const SizedBox( + height: 8, + ), + Stack( + children: [ + GestureDetector( + onTap: () async { + // open a dialog to choose between camera and gallery + var result = await showModalBottomSheet( + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(8.0), + color: Colors.black, + child: ImagePicker( + imagePickerConfig: widget.options.imagePickerConfig, + imagePickerTheme: widget.options.imagePickerTheme, + ), + ), + ); + if (result != null) { + setState(() { + image = result; + }); + } + checkIfEditingDone(); + }, + child: image != null + ? ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.memory( + image!, + width: double.infinity, + height: 150.0, + fit: BoxFit.cover, + // give it a rounded border + ), + ) + : CustomPaint( + painter: DashedBorderPainter( + color: theme.textTheme.displayMedium?.color ?? + Colors.white, + dashLength: 4.0, + dashWidth: 1.5, + space: 4, + ), + child: const SizedBox( + width: double.infinity, + height: 150.0, + child: Icon( + Icons.image, + size: 32, + ), + ), + ), + ), + // if an image is selected, show a delete button + if (image != null) ...[ + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () { + setState(() { + image = null; + }); + checkIfEditingDone(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(8.0), + ), + child: const Icon( + Icons.delete, + color: Colors.white, + ), + ), + ), + ), + ], + ], + ), + + const SizedBox(height: 16), + + Text( + widget.options.translations.commentsTitle, + style: theme.textTheme.displaySmall, + ), + Text( + widget.options.translations.allowCommentsDescription, + style: theme.textTheme.bodyMedium, + ), + // radio buttons for yes or no + Switch( + value: allowComments, + onChanged: (newValue) { + setState(() { + allowComments = newValue; + }); + }, + ), + const Spacer(), + Align( + alignment: Alignment.bottomCenter, + child: (widget.options.buttonBuilder != null) + ? widget.options.buttonBuilder!( + context, + () {}, + widget.options.translations.checkPost, + enabled: editingDone, + ) + : ElevatedButton( + onPressed: editingDone ? () {} : null, + child: Text( + widget.options.translations.checkPost, + style: theme.textTheme.bodyMedium, + ), + ), + ), + ], + ), + ); + } } diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index d633ddd..6e1bcd6 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart index 4d8a566..434e53c 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; diff --git a/packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart b/packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart new file mode 100644 index 0000000..cde436b --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; + +class DashedBorderPainter extends CustomPainter { + DashedBorderPainter({ + this.color = Colors.black, + this.dashWidth = 2.0, + this.dashLength = 6.0, + this.space = 3.0, + }); + final Color color; + final double dashWidth; + final double dashLength; + final double space; + + @override + void paint(Canvas canvas, Size size) { + var paint = Paint() + ..color = color + ..strokeWidth = dashWidth; + + var x = 0.0; + var y = 0.0; + + // Top border + while (x < size.width) { + canvas.drawLine(Offset(x, 0), Offset(x + dashLength, 0), paint); + x += dashLength + space; + } + + // Right border + while (y < size.height) { + canvas.drawLine( + Offset(size.width, y), + Offset(size.width, y + dashLength), + paint, + ); + y += dashLength + space; + } + + x = size.width; + // Bottom border + while (x > 0) { + canvas.drawLine( + Offset(x, size.height), + Offset(x - dashLength, size.height), + paint, + ); + x -= dashLength + space; + } + + y = size.height; + // Left border + while (y > 0) { + canvas.drawLine(Offset(0, y), Offset(0, y - dashLength), paint); + y -= dashLength + space; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/packages/flutter_timeline_view/pubspec.yaml b/packages/flutter_timeline_view/pubspec.yaml index 470fe67..5a69928 100644 --- a/packages/flutter_timeline_view/pubspec.yaml +++ b/packages/flutter_timeline_view/pubspec.yaml @@ -14,11 +14,18 @@ environment: dependencies: flutter: sdk: flutter + intl: any + cached_network_image: ^3.2.2 + flutter_timeline_interface: git: url: https://github.com/Iconica-Development/flutter_timeline.git path: packages/flutter_timeline_interface ref: 0.0.1 + flutter_image_picker: + git: + url: https://github.com/Iconica-Development/flutter_image_picker + ref: 1.0.4 dev_dependencies: flutter_lints: ^2.0.0