diff --git a/README.md b/README.md index b465115..5c29ace 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ final GoRouter _router = GoRouter( ); }, ), - ...getTimelineStoryRoutes() + ...getTimelineStoryRoutes(timelineUserStoryConfiguration) ], ); ``` diff --git a/packages/flutter_timeline/example/lib/apps/go_router/app.dart b/packages/flutter_timeline/example/lib/apps/go_router/app.dart new file mode 100644 index 0000000..ef18fef --- /dev/null +++ b/packages/flutter_timeline/example/lib/apps/go_router/app.dart @@ -0,0 +1,37 @@ +import 'package:example/config/config.dart'; +import 'package:example/services/timeline_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; +import 'package:go_router/go_router.dart'; + +List getTimelineRoutes() => getTimelineStoryRoutes( + getConfig( + TestTimelineService(), + ), + ); + +final _router = GoRouter( + initialLocation: '/timeline', + routes: [ + ...getTimelineRoutes(), + ], +); + +class GoRouterApp extends StatelessWidget { + const GoRouterApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + title: 'Flutter Timeline', + theme: ThemeData( + colorScheme: + ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith( + background: const Color(0xFFB8E2E8), + ), + useMaterial3: true, + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/apps/navigator/app.dart b/packages/flutter_timeline/example/lib/apps/navigator/app.dart new file mode 100644 index 0000000..2473d64 --- /dev/null +++ b/packages/flutter_timeline/example/lib/apps/navigator/app.dart @@ -0,0 +1,71 @@ +import 'package:example/config/config.dart'; +import 'package:example/services/timeline_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; + +class NavigatorApp extends StatelessWidget { + const NavigatorApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Timeline', + theme: ThemeData( + colorScheme: + ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith( + background: const Color(0xFFB8E2E8), + ), + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + var timelineService = TestTimelineService(); + var timelineOptions = options; + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: 'btn1', + onPressed: () => + createPost(context, timelineService, timelineOptions), + child: const Icon( + Icons.edit, + color: Colors.white, + ), + ), + const SizedBox( + height: 8, + ), + FloatingActionButton( + heroTag: 'btn2', + onPressed: () => generatePost(timelineService), + child: const Icon( + Icons.add, + color: Colors.white, + ), + ), + ], + ), + body: SafeArea( + child: timeLineNavigatorUserStory(getConfig(timelineService), context), + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/apps/widgets/app.dart b/packages/flutter_timeline/example/lib/apps/widgets/app.dart new file mode 100644 index 0000000..8367742 --- /dev/null +++ b/packages/flutter_timeline/example/lib/apps/widgets/app.dart @@ -0,0 +1,95 @@ +import 'package:example/config/config.dart'; +import 'package:example/services/timeline_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; + +class WidgetApp extends StatelessWidget { + const WidgetApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Timeline', + theme: ThemeData( + colorScheme: + ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith( + background: const Color(0xFFB8E2E8), + ), + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + var timelineService = TestTimelineService(); + var timelineOptions = options; + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + onPressed: () { + createPost(context, timelineService, timelineOptions); + }, + child: const Icon( + Icons.edit, + color: Colors.white, + ), + ), + const SizedBox( + height: 8, + ), + FloatingActionButton( + onPressed: () { + generatePost(timelineService); + }, + child: const Icon( + Icons.add, + color: Colors.white, + ), + ), + ], + ), + body: SafeArea( + child: TimelineScreen( + userId: 'test_user', + service: timelineService, + options: timelineOptions, + onPostTap: (post) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: TimelinePostScreen( + userId: 'test_user', + service: timelineService, + options: timelineOptions, + post: post, + onPostDelete: () { + timelineService.deletePost(post); + Navigator.of(context).pop(); + }, + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/post_screen.dart b/packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart similarity index 100% rename from packages/flutter_timeline/example/lib/post_screen.dart rename to packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart diff --git a/packages/flutter_timeline/example/lib/config/config.dart b/packages/flutter_timeline/example/lib/config/config.dart new file mode 100644 index 0000000..046615f --- /dev/null +++ b/packages/flutter_timeline/example/lib/config/config.dart @@ -0,0 +1,75 @@ +import 'package:example/apps/widgets/screens/post_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; + +TimelineUserStoryConfiguration getConfig(TimelineService service) { + return TimelineUserStoryConfiguration( + service: service, + userService: TestUserService(), + userId: 'test_user', + optionsBuilder: (context) => options); +} + +var options = TimelineOptions( + textInputBuilder: null, + padding: const EdgeInsets.all(20).copyWith(top: 28), + allowAllDeletion: true, + categoriesBuilder: (context) => [ + const TimelineCategory( + key: null, + title: 'All', + icon: SizedBox.shrink(), + ), + const TimelineCategory( + key: 'category1', + title: 'Category 1', + icon: SizedBox.shrink(), + ), + const TimelineCategory( + key: 'category2', + title: 'Category 2', + icon: SizedBox.shrink(), + ), + ], +); + +void createPost(BuildContext context, TimelineService service, + TimelineOptions options) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: TimelinePostCreationScreen( + postCategory: null, + userId: 'test_user', + service: service, + options: options, + onPostCreated: (post) { + Navigator.of(context).pop(); + }, + ), + ), + ), + ); +} + +void generatePost(TimelineService service) { + var amountOfPosts = service.getPosts(null).length; + + service.createPost( + TimelinePost( + id: 'Post$amountOfPosts', + creatorId: 'test_user', + title: 'Post $amountOfPosts', + category: amountOfPosts % 2 == 0 ? 'category1' : 'category2', + content: "Post $amountOfPosts content", + likes: 0, + reaction: 0, + createdAt: DateTime.now(), + reactionEnabled: amountOfPosts % 2 == 0 ? false : true, + imageUrl: amountOfPosts % 3 != 0 + ? 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2' + : null, + ), + ); +} diff --git a/packages/flutter_timeline/example/lib/main.dart b/packages/flutter_timeline/example/lib/main.dart index c383657..9f32ce9 100644 --- a/packages/flutter_timeline/example/lib/main.dart +++ b/packages/flutter_timeline/example/lib/main.dart @@ -1,126 +1,15 @@ -import 'package:example/timeline_service.dart'; +// import 'package:example/apps/go_router/app.dart'; +// import 'package:example/apps/navigator/app.dart'; +import 'package:example/apps/widgets/app.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:intl/date_symbol_data_local.dart'; void main() { initializeDateFormatting(); - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Timeline', - theme: ThemeData( - colorScheme: - ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith( - background: const Color(0xFFB8E2E8), - ), - useMaterial3: true, - ), - home: const MyHomePage(), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({ - super.key, - }); - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - var timelineService = TestTimelineService(); - var timelineOptions = TimelineOptions( - textInputBuilder: null, - padding: const EdgeInsets.all(20).copyWith(top: 28), - allowAllDeletion: true, - ); - - @override - Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - onPressed: () { - createPost(); - }, - child: const Icon( - Icons.edit, - color: Colors.white, - ), - ), - const SizedBox( - height: 8, - ), - FloatingActionButton( - onPressed: () { - generatePost(); - }, - child: const Icon( - Icons.add, - color: Colors.white, - ), - ), - ], - ), - body: SafeArea( - child: TimelineScreen( - userId: 'test_user', - service: timelineService, - options: timelineOptions, - ), - ), - ); - } - - void createPost() async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Scaffold( - body: TimelinePostCreationScreen( - postCategory: 'text', - userId: 'test_user', - service: timelineService, - options: timelineOptions, - onPostCreated: (post) { - Navigator.of(context).pop(); - }, - ), - ), - ), - ); - } - - void generatePost() { - var amountOfPosts = timelineService.getPosts('text').length; - - timelineService.createPost( - TimelinePost( - id: 'Post$amountOfPosts', - creatorId: 'test_user', - title: 'Post $amountOfPosts', - category: 'text', - content: "Post $amountOfPosts content", - likes: 0, - reaction: 0, - createdAt: DateTime.now(), - reactionEnabled: amountOfPosts % 2 == 0 ? false : true, - imageUrl: amountOfPosts % 3 != 0 - ? 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2' - : null, - ), - ); - } + // Uncomment any, but only one, of these lines to run the example with specific navigation. + + runApp(const WidgetApp()); + // runApp(const NavigatorApp()); + // runApp(const GoRouterApp()); } diff --git a/packages/flutter_timeline/example/lib/timeline_service.dart b/packages/flutter_timeline/example/lib/services/timeline_service.dart similarity index 98% rename from packages/flutter_timeline/example/lib/timeline_service.dart rename to packages/flutter_timeline/example/lib/services/timeline_service.dart index fe69001..3788e41 100644 --- a/packages/flutter_timeline/example/lib/timeline_service.dart +++ b/packages/flutter_timeline/example/lib/services/timeline_service.dart @@ -72,7 +72,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future> fetchPosts(String? category) async { - var posts = getMockedPosts(); + posts = getMockedPosts(); notifyListeners(); return posts; } @@ -175,7 +175,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { id: 'Post0', creatorId: 'test_user', title: 'Post 0', - category: 'text', + category: null, content: "Post 0 content", likes: 0, reaction: 0, diff --git a/packages/flutter_timeline/example/pubspec.yaml b/packages/flutter_timeline/example/pubspec.yaml index 27c1008..dd8dfa9 100644 --- a/packages/flutter_timeline/example/pubspec.yaml +++ b/packages/flutter_timeline/example/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_timeline: path: ../ intl: ^0.19.0 + go_router: ^13.0.1 dev_dependencies: flutter_test: diff --git a/packages/flutter_timeline/example/test/widget_test.dart b/packages/flutter_timeline/example/test/widget_test.dart deleted file mode 100644 index 092d222..0000000 --- a/packages/flutter_timeline/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// 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:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // 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/flutter_timeline.dart b/packages/flutter_timeline/lib/flutter_timeline.dart index b4af6ca..9c68eb6 100644 --- a/packages/flutter_timeline/lib/flutter_timeline.dart +++ b/packages/flutter_timeline/lib/flutter_timeline.dart @@ -5,6 +5,7 @@ /// Flutter Timeline library library flutter_timeline; +export 'package:flutter_timeline/src/flutter_timeline_navigator_userstory.dart'; export 'package:flutter_timeline/src/flutter_timeline_userstory.dart'; export 'package:flutter_timeline/src/models/timeline_configuration.dart'; export 'package:flutter_timeline/src/routes.dart'; diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart new file mode 100644 index 0000000..cc375aa --- /dev/null +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; + +Widget timeLineNavigatorUserStory( + TimelineUserStoryConfiguration configuration, + BuildContext context, +) => + _timelineScreenRoute(configuration, context); + +Widget _timelineScreenRoute( + TimelineUserStoryConfiguration configuration, + BuildContext context, +) => + TimelineScreen( + service: configuration.service, + options: configuration.optionsBuilder(context), + userId: configuration.userId, + onPostTap: (post) async => + configuration.onPostTap?.call(context, post) ?? + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + _postDetailScreenRoute(configuration, context, post), + ), + ), + onUserTap: (userId) { + configuration.onUserTap?.call(context, userId); + }, + ); + +Widget _postDetailScreenRoute( + TimelineUserStoryConfiguration configuration, + BuildContext context, + TimelinePost post, +) => + TimelinePostScreen( + userId: configuration.userId, + service: configuration.service, + options: configuration.optionsBuilder(context), + post: post, + onPostDelete: () async { + configuration.onPostDelete?.call(context, post) ?? + await configuration.service.deletePost(post); + }, + ); diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index 5ca037f..347fcd2 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -22,12 +22,16 @@ List getTimelineStoryRoutes( service: configuration.service, options: configuration.optionsBuilder(context), onPostTap: (post) async => - TimelineUserStoryRoutes.timelineViewPath(post.id), + configuration.onPostTap?.call(context, post) ?? + await context.push( + TimelineUserStoryRoutes.timelineViewPath(post.id), + ), ); + return buildScreenWithoutTransition( context: context, state: state, - child: configuration.mainPageBuilder?.call( + child: configuration.openPageBuilder?.call( context, timelineScreen, ) ?? @@ -37,73 +41,27 @@ List getTimelineStoryRoutes( ); }, ), - GoRoute( - path: TimelineUserStoryRoutes.timelineSelect, - pageBuilder: (context, state) { - var timelineSelectionWidget = TimelineSelectionScreen( - options: configuration.optionsBuilder(context), - categories: configuration.categoriesBuilder(context), - onCategorySelected: (category) async => context.push( - TimelineUserStoryRoutes.timelineCreatePath(category.name), - ), - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.postSelectionScreenBuilder?.call( - context, - timelineSelectionWidget, - ) ?? - Scaffold( - body: timelineSelectionWidget, - ), - ); - }, - ), - GoRoute( - path: TimelineUserStoryRoutes.timelineCreate, - pageBuilder: (context, state) { - var timelineCreateWidget = TimelinePostCreationScreen( - userId: configuration.userId, - options: configuration.optionsBuilder(context), - postCategory: state.pathParameters['category'] ?? '', - service: configuration.service, - onPostCreated: (post) => context.go( - TimelineUserStoryRoutes.timelineViewPath(post.id), - ), - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.postCreationScreenBuilder?.call( - context, - timelineCreateWidget, - ) ?? - Scaffold( - body: timelineCreateWidget, - ), - ); - }, - ), GoRoute( path: TimelineUserStoryRoutes.timelineView, pageBuilder: (context, state) { + var post = + configuration.service.getPost(state.pathParameters['post']!)!; + var timelinePostWidget = TimelinePostScreen( userId: configuration.userId, options: configuration.optionsBuilder(context), service: configuration.service, - post: configuration.service.getPost(state.pathParameters['post']!)!, - onPostDelete: () => context.pop(), + post: post, + onPostDelete: () => configuration.onPostDelete?.call(context, post), onUserTap: (user) => configuration.onUserTap?.call(context, user), ); - var category = configuration.categoriesBuilder(context).first; + return buildScreenWithoutTransition( context: context, state: state, - child: configuration.postScreenBuilder?.call( + child: configuration.openPageBuilder?.call( context, timelinePostWidget, - category, ) ?? Scaffold( body: timelinePostWidget, diff --git a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart index dc11f3f..c06ce3f 100644 --- a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart +++ b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart @@ -9,42 +9,29 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart'; @immutable class TimelineUserStoryConfiguration { const TimelineUserStoryConfiguration({ - required this.categoriesBuilder, - required this.optionsBuilder, required this.userId, required this.service, required this.userService, - this.mainPageBuilder, - this.postScreenBuilder, - this.postCreationScreenBuilder, - this.postSelectionScreenBuilder, + required this.optionsBuilder, + this.openPageBuilder, + this.onPostTap, this.onUserTap, + this.onPostDelete, }); final String userId; - final Function(BuildContext context, String userId)? onUserTap; - - final Widget Function(BuildContext context, Widget child)? - mainPageBuilder; - - final Widget Function( - BuildContext context, - Widget child, - TimelineCategory category, - )? postScreenBuilder; - - final Widget Function(BuildContext context, Widget child)? - postCreationScreenBuilder; - - final Widget Function(BuildContext context, Widget child)? - postSelectionScreenBuilder; - final TimelineService service; final TimelineUserService userService; final TimelineOptions Function(BuildContext context) optionsBuilder; - final List Function(BuildContext context) categoriesBuilder; + final Function(BuildContext context, String userId)? onUserTap; + + final Function(BuildContext context, Widget child)? openPageBuilder; + + final Function(BuildContext context, TimelinePost post)? onPostTap; + + final Widget Function(BuildContext context, TimelinePost post)? onPostDelete; } diff --git a/packages/flutter_timeline/lib/src/routes.dart b/packages/flutter_timeline/lib/src/routes.dart index 3196bea..b6c70e5 100644 --- a/packages/flutter_timeline/lib/src/routes.dart +++ b/packages/flutter_timeline/lib/src/routes.dart @@ -4,10 +4,6 @@ mixin TimelineUserStoryRoutes { static const String timelineHome = '/timeline'; - static const String timelineCreate = '/timeline-create/:category'; - static String timelineCreatePath(String category) => - '/timeline-create/$category'; - static const String timelineSelect = '/timeline-select'; static const String timelineView = '/timeline-view/:post'; static String timelineViewPath(String postId) => '/timeline-view/$postId'; } diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart index 430e14c..b88d1d8 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; @immutable class TimelineCategory { const TimelineCategory({ - required this.name, + required this.key, required this.title, required this.icon, this.canCreate = true, this.canView = true, }); - final String name; + final String? key; final String title; final Widget icon; final bool canCreate; 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 1e08d71..df7b7e9 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart @@ -15,12 +15,12 @@ class TimelinePost { required this.id, required this.creatorId, required this.title, - required this.category, required this.content, required this.likes, required this.reaction, required this.createdAt, required this.reactionEnabled, + this.category, this.creator, this.likedBy, this.reactions, @@ -67,7 +67,7 @@ class TimelinePost { final String title; /// The category of the post on which can be filtered. - final String category; + final String? category; /// The url of the image of the post. final String? imageUrl; diff --git a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart index d0b2f63..88cdb20 100644 --- a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart +++ b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart @@ -12,4 +12,6 @@ export 'src/screens/timeline_post_creation_screen.dart'; export 'src/screens/timeline_post_screen.dart'; export 'src/screens/timeline_screen.dart'; export 'src/screens/timeline_selection_screen.dart'; +export 'src/widgets/category_selector.dart'; +export 'src/widgets/category_selector_button.dart'; export 'src/widgets/timeline_post_widget.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 index 1acfaba..5fc676f 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -40,13 +40,13 @@ class TimelineOptions { this.iconSize = 26, this.postWidgetheight, this.postPadding = const EdgeInsets.all(12.0), - this.categories, + this.categoriesBuilder, this.categoryButtonBuilder, - this.catergoryLabelBuilder, this.categorySelectorHorizontalPadding, this.filterEnabled = false, this.initialFilterWord, this.searchBarBuilder, + this.postWidget, }); /// Theming options for the timeline @@ -122,7 +122,8 @@ class TimelineOptions { /// List of categories that the user can select. /// If this is null no categories will be shown. - final List? categories; + final List Function(BuildContext context)? + categoriesBuilder; /// Abilty to override the standard category selector final Widget Function({ @@ -132,10 +133,6 @@ class TimelineOptions { required bool selected, })? categoryButtonBuilder; - /// Ability to set an proper label for the category selectors. - /// Default to category key. - final String Function(String? categoryKey)? catergoryLabelBuilder; - /// Overides the standard horizontal padding of the whole category selector. final double? categorySelectorHorizontalPadding; @@ -152,6 +149,9 @@ class TimelineOptions { Map options, ) search, )? searchBarBuilder; + + /// Override the standard postwidget + final Widget Function(TimelinePost post)? postWidget; } typedef ButtonBuilder = Widget Function( 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 3e52ab9..2238f4e 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 @@ -13,16 +13,16 @@ import 'package:flutter_timeline_view/src/config/timeline_options.dart'; class TimelinePostCreationScreen extends StatefulWidget { const TimelinePostCreationScreen({ required this.userId, - required this.postCategory, required this.onPostCreated, required this.service, required this.options, + this.postCategory, super.key, }); final String userId; - final String postCategory; + final String? postCategory; /// called when the post is created final Function(TimelinePost) onPostCreated; 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 4c9983b..467da65 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 @@ -15,7 +15,7 @@ import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart'; import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; import 'package:intl/intl.dart'; -class TimelinePostScreen extends StatefulWidget { +class TimelinePostScreen extends StatelessWidget { const TimelinePostScreen({ required this.userId, required this.service, @@ -44,10 +44,45 @@ class TimelinePostScreen extends StatefulWidget { final VoidCallback onPostDelete; @override - State createState() => _TimelinePostScreenState(); + Widget build(BuildContext context) => Scaffold( + body: _TimelinePostScreen( + userId: userId, + service: service, + options: options, + post: post, + onPostDelete: onPostDelete, + onUserTap: onUserTap, + ), + ); } -class _TimelinePostScreenState extends State { +class _TimelinePostScreen extends StatefulWidget { + const _TimelinePostScreen({ + required this.userId, + required this.service, + required this.options, + required this.post, + required this.onPostDelete, + this.onUserTap, + }); + + final String userId; + + final TimelineService service; + + final TimelineOptions options; + + final TimelinePost post; + + final Function(String userId)? onUserTap; + + final VoidCallback onPostDelete; + + @override + State<_TimelinePostScreen> createState() => _TimelinePostScreenState(); +} + +class _TimelinePostScreenState extends State<_TimelinePostScreen> { TimelinePost? post; bool isLoading = true; @@ -96,8 +131,9 @@ class _TimelinePostScreenState extends State { var dateFormat = widget.options.dateFormat ?? DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode); var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm'); + if (isLoading) { - return const Center( + const Center( child: CircularProgressIndicator(), ); } @@ -185,12 +221,7 @@ class _TimelinePostScreenState extends State { if (widget.options.allowAllDeletion || post.creator?.userId == widget.userId) PopupMenuButton( - onSelected: (value) async { - if (value == 'delete') { - await widget.service.deletePost(post); - widget.onPostDelete(); - } - }, + onSelected: (value) => widget.onPostDelete(), itemBuilder: (BuildContext context) => >[ PopupMenuItem( 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 cf8b8b7..9ed7db4 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -7,19 +7,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/flutter_timeline_view.dart'; -import 'package:flutter_timeline_view/src/widgets/category_selector.dart'; class TimelineScreen extends StatefulWidget { const TimelineScreen({ required this.userId, required this.service, required this.options, + required this.onPostTap, this.scrollController, - this.onPostTap, this.onUserTap, this.posts, this.timelineCategory, - this.postWidget, super.key, }); @@ -43,14 +41,11 @@ class TimelineScreen extends StatefulWidget { final List? posts; /// Called when a post is tapped - final Function(TimelinePost)? onPostTap; + final Function(TimelinePost) onPostTap; /// If this is not null, the user can tap on the user avatar or name final Function(String userId)? onUserTap; - /// Override the standard postwidget - final Widget Function(TimelinePost post)? postWidget; - @override State createState() => _TimelineScreenState(); } @@ -196,35 +191,13 @@ class _TimelineScreenState extends State { ...posts.map( (post) => Padding( padding: widget.options.postPadding, - child: widget.postWidget?.call(post) ?? + child: widget.options.postWidget?.call(post) ?? TimelinePostWidget( service: widget.service, userId: widget.userId, options: widget.options, post: post, - onTap: () async { - if (widget.onPostTap != null) { - widget.onPostTap!.call(post); - return; - } - - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Scaffold( - body: TimelinePostScreen( - userId: widget.userId, - service: widget.service, - options: widget.options, - post: post, - onPostDelete: () { - widget.service.deletePost(post); - }, - ), - ), - ), - ); - }, + onTap: () => widget.onPostTap(post), onTapLike: () async => service.likePost(widget.userId, post), onTapUnlike: () async => 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 817090a..f0d3cfc 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_view/flutter_timeline_view.dart'; -import 'package:flutter_timeline_view/src/widgets/category_selector_button.dart'; class CategorySelector extends StatelessWidget { const CategorySelector({ @@ -18,10 +17,12 @@ class CategorySelector extends StatelessWidget { @override Widget build(BuildContext context) { - if (options.categories == null) { + if (options.categoriesBuilder == null) { return const SizedBox.shrink(); } + var categories = options.categoriesBuilder!(context); + return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -30,37 +31,19 @@ class CategorySelector extends StatelessWidget { width: options.categorySelectorHorizontalPadding ?? max(options.padding.horizontal - 4, 0), ), - options.categoryButtonBuilder?.call( - categoryKey: null, - categoryName: - options.catergoryLabelBuilder?.call(null) ?? 'All', - onTap: () => onTapCategory(null), - selected: filter == null, - ) ?? - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: CategorySelectorButton( - category: null, - selected: filter == null, - onTap: () => onTapCategory(null), - labelBuilder: options.catergoryLabelBuilder, - ), - ), - for (var category in options.categories!) ...[ + for (var category in categories) ...[ options.categoryButtonBuilder?.call( - categoryKey: category, - categoryName: - options.catergoryLabelBuilder?.call(category) ?? category, - onTap: () => onTapCategory(category), - selected: filter == category, + categoryKey: category.key, + categoryName: category.title, + onTap: () => onTapCategory(category.key), + selected: filter == category.key, ) ?? Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: CategorySelectorButton( category: category, - selected: filter == category, - onTap: () => onTapCategory(category), - labelBuilder: options.catergoryLabelBuilder, + selected: filter == category.key, + onTap: () => onTapCategory(category.key), ), ), ], 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 8b1a5db..3245303 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,17 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; class CategorySelectorButton extends StatelessWidget { const CategorySelectorButton({ required this.category, required this.selected, required this.onTap, - this.labelBuilder, super.key, }); - final String? category; + final TimelineCategory category; final bool selected; - final String Function(String? category)? labelBuilder; final void Function() onTap; @override @@ -41,7 +40,7 @@ class CategorySelectorButton extends StatelessWidget { ), ), child: Text( - labelBuilder?.call(category) ?? category ?? 'All', + category.title, style: theme.textTheme.labelMedium?.copyWith( color: selected ? theme.colorScheme.onPrimary