diff --git a/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart b/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart index acac140..c52ef3a 100644 --- a/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart +++ b/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart @@ -5,4 +5,5 @@ library flutter_timeline_interface; export 'src/model/timeline_post.dart'; +export 'src/model/timeline_poster.dart'; export 'src/model/timeline_reaction.dart'; diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart index cbad1fc..d8cfe13 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart @@ -1,4 +1,9 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/src/model/timeline_poster.dart'; import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart'; /// A post of the timeline. @@ -14,6 +19,7 @@ class TimelinePost { required this.reaction, required this.createdAt, required this.reactionEnabled, + this.creator, this.likedBy, this.reactions, this.imageUrl, @@ -25,6 +31,9 @@ class TimelinePost { /// The unique identifier of the creator of the post. final String creatorId; + /// The creator of the post. If null it isn't loaded yet. + final TimelinePosterUserModel? creator; + /// The title of the post. final String title; diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_poster.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_poster.dart new file mode 100644 index 0000000..534810a --- /dev/null +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_poster.dart @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; + +@immutable +class TimelinePosterUserModel { + const TimelinePosterUserModel({ + required this.id, + this.firstName, + this.lastName, + this.imageUrl, + }); + + final String id; + final String? firstName; + final String? lastName; + final String? imageUrl; + + String? get fullName { + var fullName = ''; + + if (firstName != null && lastName != null) { + fullName += '$firstName $lastName'; + } else if (firstName != null) { + fullName += firstName!; + } else if (lastName != null) { + fullName += lastName!; + } + + return fullName == '' ? null : fullName; + } +} 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 7294449..1df669c 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_reaction.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_reaction.dart @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + import 'package:flutter/material.dart'; @immutable 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 8cd1f6d..e57d4b3 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -3,6 +3,7 @@ // SPDX-License-Identifier: BSD-3-Clause 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_translations.dart'; import 'package:intl/intl.dart'; @@ -15,6 +16,7 @@ class TimelineOptions { this.dateformat, this.buttonBuilder, this.textInputBuilder, + this.userAvatarBuilder, }); /// The format to display the post time in @@ -26,6 +28,8 @@ class TimelineOptions { final TextInputBuilder? textInputBuilder; + final UserAvatarBuilder? userAvatarBuilder; + /// ImagePickerTheme can be used to change the UI of the /// Image Picker Widget to change the text/icons to your liking. final ImagePickerTheme imagePickerTheme; @@ -42,9 +46,13 @@ typedef ButtonBuilder = Widget Function( bool enabled, }); - typedef TextInputBuilder = Widget Function( TextEditingController controller, Widget? suffixIcon, String hintText, ); + +typedef UserAvatarBuilder = Widget Function( + TimelinePosterUserModel user, + double size, +); 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 6e1bcd6..f2d75ec 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 @@ -11,7 +11,6 @@ class TimelinePostScreen extends StatelessWidget { super.key, }); - final TimelinePost post; @override 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 434e53c..d0be49b 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -4,16 +4,22 @@ 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/timeline_post_widget.dart'; class TimelineScreen extends StatefulWidget { const TimelineScreen({ + required this.options, required this.posts, + required this.onPostTap, this.controller, this.timelineCategoryFilter, this.timelinePostHeight = 100.0, super.key, }); + final TimelineOptions options; + final ScrollController? controller; final String? timelineCategoryFilter; @@ -22,6 +28,8 @@ class TimelineScreen extends StatefulWidget { final List posts; + final Function(TimelinePost) onPostTap; + @override State createState() => _TimelineScreenState(); } @@ -36,5 +44,22 @@ class _TimelineScreenState extends State { } @override - Widget build(BuildContext context) => const Placeholder(); + Widget build(BuildContext context) => SingleChildScrollView( + child: Column( + children: [ + for (var post in widget.posts) + if (widget.timelineCategoryFilter == null || + post.category == widget.timelineCategoryFilter) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TimelinePostWidget( + options: widget.options, + post: post, + height: widget.timelinePostHeight, + onTap: () => widget.onPostTap.call(post), + ), + ), + ], + ), + ); } 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 new file mode 100644 index 0000000..9571519 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -0,0 +1,116 @@ +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'; + +class TimelinePostWidget extends StatelessWidget { + const TimelinePostWidget({ + required this.options, + required this.post, + required this.height, + this.onTap, + super.key, + }); + + final TimelineOptions options; + + final TimelinePost post; + final double height; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return InkWell( + onTap: onTap, + child: SizedBox( + height: height, + width: double.infinity, + 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) ...[ + Flexible( + child: CachedNetworkImage( + imageUrl: post.imageUrl!, + width: double.infinity, + fit: BoxFit.fitWidth, + ), + ), + ], + // 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.content, + style: theme.textTheme.bodyMedium, + overflow: TextOverflow.fade, + ), + ], + ), + Text( + options.translations.viewPost, + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + ); + } +}