From f4990dbf5c04fc8ed954bbd5cf5940ea2060fadd Mon Sep 17 00:00:00 2001 From: DirkIconica Date: Wed, 17 Apr 2024 09:35:20 +0200 Subject: [PATCH] feat: implement figma designs --- .../example/lib/apps/go_router/app.dart | 11 ++- .../lib/apps/widgets/screens/post_screen.dart | 8 +- .../example/lib/config/config.dart | 7 ++ .../flutter_timeline/example/lib/main.dart | 6 +- .../example/test/widget_test.dart | 29 +++++++ .../flutter_timeline_gorouter_userstory.dart | 28 ++++++- .../flutter_timeline_navigator_userstory.dart | 1 + packages/flutter_timeline/pubspec.yaml | 13 ++- .../lib/src/config/timeline_options.dart | 3 +- .../lib/src/config/timeline_styles.dart | 3 + .../lib/src/config/timeline_translations.dart | 31 ++++++- .../timeline_post_creation_screen.dart | 83 ++++++++++++------- .../timeline_post_overview_screen.dart | 6 +- .../lib/src/screens/timeline_post_screen.dart | 17 ++-- .../lib/src/screens/timeline_screen.dart | 18 ++++ .../lib/src/services/local_post_service.dart | 51 ++++++++++-- .../lib/src/widgets/category_selector.dart | 39 +++++---- .../src/widgets/category_selector_button.dart | 74 +++++++++++------ .../lib/src/widgets/reaction_bottom.dart | 69 ++++++++------- .../lib/src/widgets/timeline_post_widget.dart | 8 +- 20 files changed, 363 insertions(+), 142 deletions(-) create mode 100644 packages/flutter_timeline/example/test/widget_test.dart diff --git a/packages/flutter_timeline/example/lib/apps/go_router/app.dart b/packages/flutter_timeline/example/lib/apps/go_router/app.dart index 7f222a7..9c30ade 100644 --- a/packages/flutter_timeline/example/lib/apps/go_router/app.dart +++ b/packages/flutter_timeline/example/lib/apps/go_router/app.dart @@ -25,9 +25,14 @@ class GoRouterApp extends StatelessWidget { routerConfig: _router, title: 'Flutter Timeline', theme: ThemeData( - colorScheme: - ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith( - background: const Color(0xFFB8E2E8), + textTheme: const TextTheme( + titleLarge: TextStyle( + color: Color(0xffb71c6d), fontFamily: 'Playfair Display')), + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFFB8E2E8), + primary: const Color(0xffb71c6d), + ).copyWith( + background: const Color(0XFFFAF9F6), ), useMaterial3: true, ), diff --git a/packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart b/packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart index 485b7f2..8b86dfa 100644 --- a/packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart +++ b/packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart @@ -34,7 +34,13 @@ class _PostScreenState extends State { class TestUserService implements TimelineUserService { final Map _users = { - 'test_user': const TimelinePosterUserModel(userId: 'test_user') + 'test_user': const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ) }; @override diff --git a/packages/flutter_timeline/example/lib/config/config.dart b/packages/flutter_timeline/example/lib/config/config.dart index 4d0ec24..5eede94 100644 --- a/packages/flutter_timeline/example/lib/config/config.dart +++ b/packages/flutter_timeline/example/lib/config/config.dart @@ -101,6 +101,13 @@ void generatePost(TimelineService service) { content: "Post $amountOfPosts content", likes: 0, reaction: 0, + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), createdAt: DateTime.now(), reactionEnabled: amountOfPosts % 2 == 0 ? false : true, imageUrl: amountOfPosts % 3 != 0 diff --git a/packages/flutter_timeline/example/lib/main.dart b/packages/flutter_timeline/example/lib/main.dart index 9f32ce9..0d097aa 100644 --- a/packages/flutter_timeline/example/lib/main.dart +++ b/packages/flutter_timeline/example/lib/main.dart @@ -1,6 +1,6 @@ // import 'package:example/apps/go_router/app.dart'; // import 'package:example/apps/navigator/app.dart'; -import 'package:example/apps/widgets/app.dart'; +import 'package:example/apps/go_router/app.dart'; import 'package:flutter/material.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -9,7 +9,7 @@ void main() { // Uncomment any, but only one, of these lines to run the example with specific navigation. - runApp(const WidgetApp()); + // runApp(const WidgetApp()); // runApp(const NavigatorApp()); - // runApp(const GoRouterApp()); + runApp(const GoRouterApp()); } diff --git a/packages/flutter_timeline/example/test/widget_test.dart b/packages/flutter_timeline/example/test/widget_test.dart new file mode 100644 index 0000000..f84cd57 --- /dev/null +++ b/packages/flutter_timeline/example/test/widget_test.dart @@ -0,0 +1,29 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:example/apps/widgets/app.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const WidgetApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart index dc30f03..441b70e 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart @@ -43,10 +43,16 @@ List getTimelineStoryRoutes({ ); var button = FloatingActionButton( + backgroundColor: Theme.of(context).primaryColor, onPressed: () async => context.go( TimelineUserStoryRoutes.timelinePostCreation, ), - child: const Icon(Icons.add), + shape: const CircleBorder(), + child: const Icon( + Icons.add, + color: Colors.white, + size: 30, + ), ); return buildScreenWithoutTransition( @@ -55,7 +61,13 @@ List getTimelineStoryRoutes({ child: config.homeOpenPageBuilder ?.call(context, timelineScreen, button) ?? Scaffold( - appBar: AppBar(), + appBar: AppBar( + backgroundColor: Colors.black, + title: Text( + 'Iconinstagram', + style: Theme.of(context).textTheme.titleLarge, + ), + ), body: timelineScreen, floatingActionButton: button, ), @@ -66,18 +78,19 @@ List getTimelineStoryRoutes({ path: TimelineUserStoryRoutes.timelineView, pageBuilder: (context, state) { var post = - config.service.postService.getPost(state.pathParameters['post']!)!; + config.service.postService.getPost(state.pathParameters['post']!); var timelinePostWidget = TimelinePostScreen( userId: config.userId, options: config.optionsBuilder(context), service: config.service, - post: post, + post: post!, onPostDelete: () => config.onPostDelete?.call(context, post), onUserTap: (user) => config.onUserTap?.call(context, user), ); var backButton = IconButton( + color: Colors.white, icon: const Icon(Icons.arrow_back_ios), onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome), ); @@ -90,6 +103,11 @@ List getTimelineStoryRoutes({ Scaffold( appBar: AppBar( leading: backButton, + backgroundColor: Colors.black, + title: Text( + 'Category', + style: Theme.of(context).textTheme.titleLarge, + ), ), body: timelinePostWidget, ), @@ -133,8 +151,10 @@ List getTimelineStoryRoutes({ ?.call(context, timelinePostCreationWidget, backButton) ?? Scaffold( appBar: AppBar( + backgroundColor: Colors.black, title: Text( config.optionsBuilder(context).translations.postCreation, + style: Theme.of(context).textTheme.titleLarge, ), leading: backButton, ), diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart index 9398f1a..3e3c9ee 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -134,6 +134,7 @@ Widget _postCreationScreenRoute({ return Scaffold( appBar: AppBar( title: Text( + style: Theme.of(context).textTheme.titleLarge, config.optionsBuilder(context).translations.postCreation, ), ), diff --git a/packages/flutter_timeline/pubspec.yaml b/packages/flutter_timeline/pubspec.yaml index 5261483..e799b1d 100644 --- a/packages/flutter_timeline/pubspec.yaml +++ b/packages/flutter_timeline/pubspec.yaml @@ -8,13 +8,13 @@ version: 2.3.0 publish_to: none environment: - sdk: '>=3.1.3 <4.0.0' + sdk: ">=3.1.3 <4.0.0" dependencies: flutter: sdk: flutter go_router: any - + flutter_timeline_view: git: url: https://github.com/Iconica-Development/flutter_timeline @@ -22,10 +22,10 @@ dependencies: ref: 2.3.0 flutter_timeline_interface: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_interface - ref: 2.3.0 + git: + url: https://github.com/Iconica-Development/flutter_timeline + path: packages/flutter_timeline_interface + ref: 2.3.0 dev_dependencies: flutter_lints: ^2.0.0 @@ -35,4 +35,3 @@ dev_dependencies: ref: 6.0.0 flutter: - 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 9222071..0f9aa93 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -40,7 +40,8 @@ class TimelineOptions { this.padding = const EdgeInsets.symmetric(vertical: 12.0), this.iconSize = 26, this.postWidgetHeight, - this.postPadding = const EdgeInsets.all(12.0), + this.postPadding = + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0), this.filterOptions = const FilterOptions(), this.categoriesOptions = const CategoriesOptions(), this.requireImageForPost = false, diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_styles.dart b/packages/flutter_timeline_view/lib/src/config/timeline_styles.dart index e8209c3..8343052 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_styles.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_styles.dart @@ -20,6 +20,7 @@ class TimelineTextStyles { this.postTitleStyle, this.postLikeTitleAndAmount, this.postCreatedAtStyle, + this.categoryTitleStyle, }); /// The TextStyle for the text indicating that you can view a post @@ -70,4 +71,6 @@ class TimelineTextStyles { /// The TextStyle for the creation time of the post final TextStyle? postCreatedAtStyle; + + final TextStyle? categoryTitleStyle; } 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 21d5388..1da4d9b 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart @@ -11,12 +11,15 @@ class TimelineTranslations { required this.noPosts, required this.noPostsWithFilter, required this.title, + required this.titleHintText, required this.content, + required this.contentHintText, required this.contentDescription, required this.uploadImage, required this.uploadImageDescription, required this.allowComments, required this.allowCommentsDescription, + required this.commentsTitleOnPost, required this.checkPost, required this.deletePost, required this.deleteReaction, @@ -32,6 +35,8 @@ class TimelineTranslations { required this.postOverview, required this.postIn, required this.postCreation, + required this.yes, + required this.no, }); const TimelineTranslations.empty() @@ -46,12 +51,13 @@ class TimelineTranslations { allowComments = 'Are people allowed to comment?', allowCommentsDescription = 'Indicate whether people are allowed to respond', + commentsTitleOnPost = 'Comments', checkPost = 'Check post overview', deletePost = 'Delete post', deleteReaction = 'Delete Reaction', viewPost = 'View post', likesTitle = 'Likes', - commentsTitle = 'Comments', + commentsTitle = 'Are people allowed to comment?', firstComment = 'Be the first to comment', writeComment = 'Write your comment here...', postAt = 'at', @@ -60,7 +66,11 @@ class TimelineTranslations { searchHint = 'Search...', postOverview = 'Post Overview', postIn = 'Post in', - postCreation = 'Create Post'; + postCreation = 'Create Post', + titleHintText = 'Title...', + contentHintText = 'Context...', + yes = 'Yes', + no = 'No'; final String noPosts; final String noPostsWithFilter; @@ -76,11 +86,15 @@ class TimelineTranslations { final String checkPost; final String postAt; + final String titleHintText; + final String contentHintText; + final String deletePost; final String deleteReaction; final String viewPost; final String likesTitle; final String commentsTitle; + final String commentsTitleOnPost; final String writeComment; final String firstComment; final String postLoadingError; @@ -93,6 +107,9 @@ class TimelineTranslations { final String postIn; final String postCreation; + final String yes; + final String no; + TimelineTranslations copyWith({ String? noPosts, String? noPostsWithFilter, @@ -104,6 +121,7 @@ class TimelineTranslations { String? uploadImageDescription, String? allowComments, String? allowCommentsDescription, + String? commentsTitleOnPost, String? checkPost, String? postAt, String? deletePost, @@ -119,6 +137,10 @@ class TimelineTranslations { String? postOverview, String? postIn, String? postCreation, + String? titleHintText, + String? contentHintText, + String? yes, + String? no, }) => TimelineTranslations( noPosts: noPosts ?? this.noPosts, @@ -133,6 +155,7 @@ class TimelineTranslations { allowComments: allowComments ?? this.allowComments, allowCommentsDescription: allowCommentsDescription ?? this.allowCommentsDescription, + commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost, checkPost: checkPost ?? this.checkPost, postAt: postAt ?? this.postAt, deletePost: deletePost ?? this.deletePost, @@ -149,5 +172,9 @@ class TimelineTranslations { postOverview: postOverview ?? this.postOverview, postIn: postIn ?? this.postIn, postCreation: postCreation ?? this.postCreation, + titleHintText: titleHintText ?? this.titleHintText, + contentHintText: contentHintText ?? this.contentHintText, + yes: yes ?? this.yes, + no: no ?? this.no, ); } 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 48e8d3b..ee5b6a8 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 @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: BSD-3-Clause +import 'dart:math'; import 'dart:typed_data'; import 'package:dotted_border/dotted_border.dart'; @@ -97,7 +98,7 @@ class _TimelinePostCreationScreenState Widget build(BuildContext context) { Future onPostCreated() async { var post = TimelinePost( - id: '', + id: 'Post${Random().nextInt(1000)}', creatorId: widget.userId, title: titleController.text, category: widget.postCategory, @@ -128,7 +129,7 @@ class _TimelinePostCreationScreenState children: [ Text( widget.options.translations.title, - style: theme.textTheme.displaySmall, + style: theme.textTheme.titleMedium, ), widget.options.textInputBuilder?.call( titleController, @@ -137,11 +138,14 @@ class _TimelinePostCreationScreenState ) ?? TextField( controller: titleController, + decoration: InputDecoration( + hintText: widget.options.translations.titleHintText, + ), ), const SizedBox(height: 16), Text( widget.options.translations.content, - style: theme.textTheme.displaySmall, + style: theme.textTheme.titleMedium, ), const SizedBox(height: 4), Text( @@ -157,6 +161,9 @@ class _TimelinePostCreationScreenState expands: true, maxLines: null, minLines: null, + decoration: InputDecoration( + hintText: widget.options.translations.contentHintText, + ), ), ), const SizedBox( @@ -165,7 +172,7 @@ class _TimelinePostCreationScreenState // input field for the content Text( widget.options.translations.uploadImage, - style: theme.textTheme.displaySmall, + style: theme.textTheme.titleMedium, ), Text( widget.options.translations.uploadImageDescription, @@ -198,30 +205,31 @@ class _TimelinePostCreationScreenState } checkIfEditingDone(); }, - child: image != null - ? ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: Image.memory( + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: image != null + ? Image.memory( image!, width: double.infinity, height: 150.0, fit: BoxFit.cover, // give it a rounded border - ), - ) - : DottedBorder( - radius: const Radius.circular(8.0), - color: theme.textTheme.displayMedium?.color ?? - Colors.white, - child: const SizedBox( - width: double.infinity, - height: 150.0, - child: Icon( - Icons.image, - size: 32, + ) + : DottedBorder( + dashPattern: const [4, 4], + radius: const Radius.circular(8.0), + color: theme.textTheme.displayMedium?.color ?? + Colors.white, + child: const SizedBox( + width: double.infinity, + height: 150.0, + child: Icon( + Icons.image, + size: 50, + ), ), ), - ), + ), ), // if an image is selected, show a delete button if (image != null) ...[ @@ -255,21 +263,36 @@ class _TimelinePostCreationScreenState Text( widget.options.translations.commentsTitle, - style: theme.textTheme.displaySmall, + style: theme.textTheme.titleMedium, ), Text( widget.options.translations.allowCommentsDescription, style: theme.textTheme.bodyMedium, ), - // radio buttons for yes or no - Switch( - value: allowComments, - onChanged: (newValue) { - setState(() { - allowComments = newValue; - }); - }, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Checkbox( + value: allowComments, + onChanged: (value) { + setState(() { + allowComments = true; + }); + }, + ), + Text(widget.options.translations.yes), + Checkbox( + value: !allowComments, + onChanged: (value) { + setState(() { + allowComments = false; + }); + }, + ), + Text(widget.options.translations.no), + ], ), + Align( alignment: Alignment.bottomCenter, child: (widget.options.buttonBuilder != null) diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_overview_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_overview_screen.dart index 0f75694..347ab1b 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_overview_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_overview_screen.dart @@ -21,7 +21,11 @@ class TimelinePostOverviewScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(options.translations.postOverview), + backgroundColor: Colors.black, + title: Text( + options.translations.postOverview, + style: TextStyle(color: Theme.of(context).primaryColor), + ), ), body: Column( children: [ 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 6f37faf..c6a10a9 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 @@ -93,6 +93,10 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { decoration: InputDecoration( hintText: hintText, suffixIcon: suffixIcon, + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(20.0), // Adjust the value as needed + ), ), ); @@ -184,7 +188,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { if (post.creator!.imageUrl != null) ...[ widget.options.userAvatarBuilder?.call( post.creator!, - 40, + 28, ) ?? CircleAvatar( radius: 20, @@ -196,7 +200,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { ] else ...[ widget.options.anonymousAvatarBuilder?.call( post.creator!, - 40, + 28, ) ?? const CircleAvatar( radius: 20, @@ -318,6 +322,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { Icon( Icons.thumb_up_rounded, color: widget.options.theme.iconColor, + size: widget.options.iconSize, ), ), ), @@ -418,8 +423,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { const SizedBox(height: 20), if (post.reactionEnabled) ...[ Text( - widget.options.translations.commentsTitle, - style: theme.textTheme.displaySmall, + widget.options.translations.commentsTitleOnPost, + style: theme.textTheme.titleMedium, ), for (var reaction in post.reactions ?? []) ...[ @@ -469,7 +474,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { reaction.creator!.imageUrl!.isNotEmpty) ...[ widget.options.userAvatarBuilder?.call( reaction.creator!, - 25, + 28, ) ?? CircleAvatar( radius: 20, @@ -480,7 +485,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> { ] else ...[ widget.options.anonymousAvatarBuilder?.call( reaction.creator!, - 25, + 28, ) ?? const CircleAvatar( radius: 20, 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 ca812f5..d7a256c 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -74,10 +74,27 @@ class _TimelineScreenState extends State { late var filterWord = widget.options.filterOptions.initialFilterWord; + bool _isOnTop = true; + + @override + void dispose() { + controller.removeListener(_updateIsOnTop); + controller.dispose(); + super.dispose(); + } + + void _updateIsOnTop() { + setState(() { + _isOnTop = controller.position.pixels < 40; + }); + } + @override void initState() { super.initState(); controller = widget.scrollController ?? ScrollController(); + controller.addListener(_updateIsOnTop); + // only load the posts after the first frame WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(loadPosts()); @@ -188,6 +205,7 @@ class _TimelineScreenState extends State { ), ], CategorySelector( + isOnTop: _isOnTop, filter: category, options: widget.options, onTapCategory: (categoryKey) { diff --git a/packages/flutter_timeline_view/lib/src/services/local_post_service.dart b/packages/flutter_timeline_view/lib/src/services/local_post_service.dart index 3989df9..9f5b735 100644 --- a/packages/flutter_timeline_view/lib/src/services/local_post_service.dart +++ b/packages/flutter_timeline_view/lib/src/services/local_post_service.dart @@ -17,7 +17,13 @@ class LocalTimelinePostService Future createPost(TimelinePost post) async { posts.add( post.copyWith( - creator: const TimelinePosterUserModel(userId: 'test_user'), + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), ), ); notifyListeners(); @@ -62,7 +68,13 @@ class LocalTimelinePostService for (var reaction in reactions) { updatedReactions.add( reaction.copyWith( - creator: const TimelinePosterUserModel(userId: 'test_user'), + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), ), ); } @@ -156,7 +168,13 @@ class LocalTimelinePostService var updatedReaction = reaction.copyWith( id: reactionId, - creator: const TimelinePosterUserModel(userId: 'test_user'), + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), ); var updatedPost = post.copyWith( @@ -179,11 +197,20 @@ class LocalTimelinePostService creatorId: 'test_user', title: 'Post 0', category: null, + imageUrl: + 'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg', content: 'Standard post without image made by the current user', likes: 0, reaction: 0, createdAt: DateTime.now(), reactionEnabled: false, + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), ), TimelinePost( id: 'Post1', @@ -197,7 +224,14 @@ class LocalTimelinePostService createdAt: DateTime.now(), reactionEnabled: false, imageUrl: - 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2', + 'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg', + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), ), TimelinePost( id: 'Post2', @@ -211,7 +245,14 @@ class LocalTimelinePostService createdAt: DateTime.now(), reactionEnabled: true, imageUrl: - 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2', + 'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg', + creator: const TimelinePosterUserModel( + userId: 'test_user', + imageUrl: + 'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop', + firstName: 'Dirk', + lastName: 'lukassen', + ), ), ]; } diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart index 858d0b9..4d9dc4b 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart @@ -3,55 +3,64 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_view/flutter_timeline_view.dart'; -class CategorySelector extends StatelessWidget { +class CategorySelector extends StatefulWidget { const CategorySelector({ required this.filter, required this.options, required this.onTapCategory, + required this.isOnTop, super.key, }); final String? filter; final TimelineOptions options; final void Function(String? categoryKey) onTapCategory; + final bool isOnTop; + @override + State createState() => _CategorySelectorState(); +} + +class _CategorySelectorState extends State { @override Widget build(BuildContext context) { - if (options.categoriesOptions.categoriesBuilder == null) { + if (widget.options.categoriesOptions.categoriesBuilder == null) { return const SizedBox.shrink(); } - var categories = options.categoriesOptions.categoriesBuilder!(context); - + var categories = + widget.options.categoriesOptions.categoriesBuilder!(context); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ SizedBox( - width: - options.categoriesOptions.categorySelectorHorizontalPadding ?? - max(options.padding.horizontal - 4, 0), + width: widget.options.categoriesOptions + .categorySelectorHorizontalPadding ?? + max(widget.options.padding.horizontal - 20, 0), ), for (var category in categories) ...[ - options.categoriesOptions.categoryButtonBuilder?.call( + widget.options.categoriesOptions.categoryButtonBuilder?.call( categoryKey: category.key, categoryName: category.title, - onTap: () => onTapCategory(category.key), - selected: filter == category.key, + onTap: () => widget.onTapCategory(category.key), + selected: widget.filter == category.key, ) ?? Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: CategorySelectorButton( + isOnTop: widget.isOnTop, category: category, - selected: filter == category.key, - onTap: () => onTapCategory(category.key), + selected: widget.filter == category.key, + onTap: () => widget.onTapCategory(category.key), + options: widget.options, ), ), ], SizedBox( - width: - options.categoriesOptions.categorySelectorHorizontalPadding ?? - max(options.padding.horizontal - 4, 0), + width: widget.options.categoriesOptions + .categorySelectorHorizontalPadding ?? + max(widget.options.padding.horizontal - 4, 0), ), ], ), diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart index 3245303..e5f7840 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart @@ -1,50 +1,76 @@ import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; class CategorySelectorButton extends StatelessWidget { const CategorySelectorButton({ required this.category, required this.selected, required this.onTap, + required this.options, + required this.isOnTop, super.key, }); final TimelineCategory category; final bool selected; final void Function() onTap; + final TimelineOptions options; + final bool isOnTop; @override Widget build(BuildContext context) { var theme = Theme.of(context); - return TextButton( - onPressed: onTap, - style: ButtonStyle( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const MaterialStatePropertyAll( - EdgeInsets.symmetric( - vertical: 5, - horizontal: 12, + return AnimatedContainer( + height: isOnTop ? 140 : 40, + duration: const Duration(milliseconds: 100), + child: TextButton( + onPressed: onTap, + style: ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const MaterialStatePropertyAll( + EdgeInsets.symmetric( + vertical: 5, + horizontal: 12, + ), ), - ), - minimumSize: const MaterialStatePropertyAll(Size.zero), - backgroundColor: MaterialStatePropertyAll( - selected ? theme.colorScheme.primary : theme.colorScheme.surface, - ), - shape: const MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(45), + fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)), + backgroundColor: MaterialStatePropertyAll( + selected ? theme.colorScheme.primary : Colors.transparent, + ), + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + side: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), ), ), ), - ), - child: Text( - category.title, - style: theme.textTheme.labelMedium?.copyWith( - color: selected - ? theme.colorScheme.onPrimary - : theme.colorScheme.onSurface, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: + isOnTop ? MainAxisAlignment.end : MainAxisAlignment.center, + children: [ + Text( + category.title, + style: (options.theme.textStyles.categoryTitleStyle ?? + theme.textTheme.labelLarge) + ?.copyWith( + color: selected + ? theme.colorScheme.onPrimary + : theme.colorScheme.onSurface, + ), + ), + ], + ), + ], ), ), ); diff --git a/packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart b/packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart index 9895112..2118f60 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/reaction_bottom.dart @@ -30,46 +30,43 @@ class _ReactionBottomState extends State { final TextEditingController _textEditingController = TextEditingController(); @override - Widget build(BuildContext context) => Container( - color: Theme.of(context).colorScheme.background, + Widget build(BuildContext context) => SafeArea( + bottom: true, 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, + color: Theme.of(context).colorScheme.background, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + height: 48, + child: widget.messageInputBuilder( + _textEditingController, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + 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, ), - 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 ff1976b..ffff6c9 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 @@ -69,7 +69,7 @@ class _TimelinePostWidgetState extends State { if (widget.post.creator!.imageUrl != null) ...[ widget.options.userAvatarBuilder?.call( widget.post.creator!, - 40, + 28, ) ?? CircleAvatar( radius: 20, @@ -213,7 +213,7 @@ class _TimelinePostWidgetState extends State { icon: widget.options.theme.likeIcon ?? Icon( widget.post.likedBy?.contains(widget.userId) ?? false - ? Icons.favorite + ? Icons.favorite_rounded : Icons.favorite_outline_outlined, ), label: Text('${widget.post.likes}'), @@ -240,7 +240,7 @@ class _TimelinePostWidgetState extends State { color: Colors.transparent, child: widget.options.theme.likedIcon ?? Icon( - Icons.thumb_up_rounded, + Icons.favorite_rounded, color: widget.options.theme.iconColor, size: widget.options.iconSize, ), @@ -253,7 +253,7 @@ class _TimelinePostWidgetState extends State { color: Colors.transparent, child: widget.options.theme.likedIcon ?? Icon( - Icons.thumb_up_rounded, + Icons.favorite_outline, color: widget.options.theme.iconColor, size: widget.options.iconSize, ),