From fb8ca56a876f1bd0dede7b435e933396e8e190b4 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Sun, 19 Nov 2023 23:11:53 +0100 Subject: [PATCH] feat: add timeline post detail screen --- .../lib/src/model/timeline_reaction.dart | 5 + .../lib/src/config/timeline_options.dart | 11 +- .../lib/src/config/timeline_theme.dart | 14 ++ .../lib/src/config/timeline_translations.dart | 2 + .../timeline_post_creation_screen.dart | 14 +- .../lib/src/screens/timeline_post_screen.dart | 171 +++++++++++++++++- .../lib/src/widgets/dotted_container.dart | 65 ------- .../lib/src/widgets/reaction_bottom.dart | 76 ++++++++ .../lib/src/widgets/timeline_post_widget.dart | 2 +- packages/flutter_timeline_view/pubspec.yaml | 3 +- 10 files changed, 285 insertions(+), 78 deletions(-) create mode 100644 packages/flutter_timeline_view/lib/src/config/timeline_theme.dart delete mode 100644 packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart create mode 100644 packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_reaction.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_reaction.dart index 1df669c..868e8d6 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_reaction.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_reaction.dart @@ -3,6 +3,7 @@ // SPDX-License-Identifier: BSD-3-Clause import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/src/model/timeline_poster.dart'; @immutable class TimelinePostReaction { @@ -12,6 +13,7 @@ class TimelinePostReaction { required this.creatorId, required this.reaction, required this.createdAt, + this.creator, }); /// The unique identifier of the reaction. @@ -23,6 +25,9 @@ class TimelinePostReaction { /// The unique identifier of the creator of the reaction. final String creatorId; + /// The creator of the post. If null it isn't loaded yet. + final TimelinePosterUserModel? creator; + /// The reactiontext final String reaction; diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart index e57d4b3..5649978 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -4,24 +4,33 @@ import 'package:flutter/material.dart'; import 'package:flutter_image_picker/flutter_image_picker.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/src/config/timeline_theme.dart'; import 'package:flutter_timeline_view/src/config/timeline_translations.dart'; import 'package:intl/intl.dart'; @immutable class TimelineOptions { const TimelineOptions({ + this.theme = const TimelineTheme(), this.translations = const TimelineTranslations(), this.imagePickerConfig = const ImagePickerConfig(), this.imagePickerTheme = const ImagePickerTheme(), this.dateformat, + this.timeFormat, this.buttonBuilder, this.textInputBuilder, this.userAvatarBuilder, }); - /// The format to display the post time in + /// Theming options for the timeline + final TimelineTheme theme; + + /// The format to display the post date in final DateFormat? dateformat; + /// The format to display the post time in + final DateFormat? timeFormat; + final TimelineTranslations translations; final ButtonBuilder? buttonBuilder; diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart b/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart new file mode 100644 index 0000000..57d8235 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/config/timeline_theme.dart @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; + +@immutable +class TimelineTheme { + const TimelineTheme({ + this.iconColor, + }); + + final Color? iconColor; +} diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart index 25c04c0..df33ff7 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart @@ -21,6 +21,7 @@ class TimelineTranslations { this.likesTitle = 'Likes', this.commentsTitle = 'Comments', this.writeComment = 'Write your comment here...', + this.postAt = 'at', }); final String title; @@ -31,6 +32,7 @@ class TimelineTranslations { final String allowComments; final String allowCommentsDescription; final String checkPost; + final String postAt; final String deletePost; final String viewPost; 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 dcd806a..b4a88ea 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 @@ -4,10 +4,10 @@ import 'dart:typed_data'; +import 'package:dotted_border/dotted_border.dart'; 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({ @@ -147,14 +147,10 @@ class _TimelinePostCreationScreenState // give it a rounded border ), ) - : CustomPaint( - painter: DashedBorderPainter( - color: theme.textTheme.displayMedium?.color ?? - Colors.white, - dashLength: 4.0, - dashWidth: 1.5, - space: 4, - ), + : DottedBorder( + radius: const Radius.circular(8.0), + color: theme.textTheme.displayMedium?.color ?? + Colors.white, child: const SizedBox( width: double.infinity, height: 150.0, 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 f2d75ec..cf4a953 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 @@ -2,17 +2,186 @@ // // SPDX-License-Identifier: BSD-3-Clause +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/src/config/timeline_options.dart'; +import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart'; +import 'package:intl/intl.dart'; class TimelinePostScreen extends StatelessWidget { const TimelinePostScreen({ + required this.options, required this.post, + this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16), super.key, }); + final TimelineOptions options; + final TimelinePost post; + /// The padding around the screen + final EdgeInsets padding; + @override - Widget build(BuildContext context) => const Placeholder(); + Widget build(BuildContext context) { + var theme = Theme.of(context); + var dateFormat = options.dateformat ?? + DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode); + var timeFormat = options.timeFormat ?? DateFormat('HH:mm'); + return Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (post.creator != null) + Row( + children: [ + if (post.creator!.imageUrl != null) ...[ + options.userAvatarBuilder?.call( + post.creator!, + 40, + ) ?? + CircleAvatar( + radius: 20, + backgroundImage: CachedNetworkImageProvider( + post.creator!.imageUrl!, + ), + ), + ], + const SizedBox(width: 10), + if (post.creator!.fullName != null) ...[ + Text( + post.creator!.fullName!, + style: theme.textTheme.titleMedium, + ), + ], + + // three small dots at the end + const Spacer(), + const Icon(Icons.more_horiz), + ], + ), + const SizedBox(height: 8), + // image of the post + if (post.imageUrl != null) ...[ + CachedNetworkImage( + imageUrl: post.imageUrl!, + width: double.infinity, + fit: BoxFit.fitHeight, + ), + ], + // post information + Row( + children: [ + // like icon + IconButton( + onPressed: () {}, + icon: const Icon(Icons.thumb_up_rounded), + ), + // comment icon + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.chat_bubble_outline_rounded, + ), + ), + ], + ), + Text( + '${post.likes} ${options.translations.likesTitle}', + style: theme.textTheme.titleSmall, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + post.creator?.fullName ?? '', + style: theme.textTheme.titleSmall, + ), + const SizedBox(width: 8), + Text( + post.title, + style: theme.textTheme.bodyMedium, + overflow: TextOverflow.fade, + ), + ], + ), + Text( + post.content, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 4), + Text( + '${dateFormat.format(post.createdAt)} ' + '${options.translations.postAt} ' + '${timeFormat.format(post.createdAt)}', + style: theme.textTheme.bodySmall, + ), + const SizedBox(height: 12), + + Text( + options.translations.commentsTitle, + style: theme.textTheme.displaySmall, + ), + for (var reaction + in post.reactions ?? []) ...[ + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (reaction.creator?.imageUrl != null && + reaction.creator!.imageUrl!.isNotEmpty) ...[ + options.userAvatarBuilder?.call( + reaction.creator!, + 25, + ) ?? + CircleAvatar( + radius: 20, + backgroundImage: CachedNetworkImageProvider( + reaction.creator!.imageUrl!, + ), + ), + ], + const SizedBox(width: 10), + if (reaction.creator?.fullName != null) ...[ + Text( + reaction.creator!.fullName!, + style: theme.textTheme.titleSmall, + ), + ], + const SizedBox(width: 10), + Expanded( + child: Text( + reaction.reaction, + style: theme.textTheme.bodyMedium, + // text should go to new line + softWrap: true, + ), + ), + ], + ), + const SizedBox(height: 100), + ], + ], + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: ReactionBottom( + messageInputBuilder: options.textInputBuilder!, + onPressSelectImage: () async {}, + onReactionSubmit: (reaction) async {}, + translations: options.translations, + iconColor: options.theme.iconColor, + ), + ), + ], + ); + } } diff --git a/packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart b/packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart deleted file mode 100644 index cde436b..0000000 --- a/packages/flutter_timeline_view/lib/src/widgets/dotted_container.dart +++ /dev/null @@ -1,65 +0,0 @@ -// 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/lib/src/widgets/reaction_bottom.dart b/packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart new file mode 100644 index 0000000..9895112 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_view/src/config/timeline_options.dart'; +import 'package:flutter_timeline_view/src/config/timeline_translations.dart'; + +class ReactionBottom extends StatefulWidget { + const ReactionBottom({ + required this.onReactionSubmit, + required this.messageInputBuilder, + required this.translations, + this.onPressSelectImage, + this.iconColor, + super.key, + }); + + final Future Function(String text) onReactionSubmit; + final TextInputBuilder messageInputBuilder; + final VoidCallback? onPressSelectImage; + final TimelineTranslations translations; + final Color? iconColor; + + @override + State createState() => _ReactionBottomState(); +} + +class _ReactionBottomState extends State { + final TextEditingController _textEditingController = TextEditingController(); + + @override + Widget build(BuildContext context) => Container( + color: Theme.of(context).colorScheme.background, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + height: 45, + child: widget.messageInputBuilder( + _textEditingController, + Padding( + padding: const EdgeInsets.only(right: 15.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: widget.onPressSelectImage, + icon: Icon( + Icons.image, + color: widget.iconColor, + ), + ), + IconButton( + onPressed: () async { + var value = _textEditingController.text; + + if (value.isNotEmpty) { + await widget.onReactionSubmit(value); + _textEditingController.clear(); + } + }, + icon: Icon( + Icons.send, + color: widget.iconColor, + ), + ), + ], + ), + ), + widget.translations.writeComment, + ), + ), + ); +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 9571519..277bec3 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -98,7 +98,7 @@ class TimelinePostWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - post.content, + post.title, style: theme.textTheme.bodyMedium, overflow: TextOverflow.fade, ), diff --git a/packages/flutter_timeline_view/pubspec.yaml b/packages/flutter_timeline_view/pubspec.yaml index 5a69928..956c698 100644 --- a/packages/flutter_timeline_view/pubspec.yaml +++ b/packages/flutter_timeline_view/pubspec.yaml @@ -16,7 +16,8 @@ dependencies: sdk: flutter intl: any cached_network_image: ^3.2.2 - + dotted_border: ^2.1.0 + flutter_timeline_interface: git: url: https://github.com/Iconica-Development/flutter_timeline.git