diff --git a/packages/flutter_timeline/example/lib/apps/navigator/app.dart b/packages/flutter_timeline/example/lib/apps/navigator/app.dart index 0cbed13..e13c783 100644 --- a/packages/flutter_timeline/example/lib/apps/navigator/app.dart +++ b/packages/flutter_timeline/example/lib/apps/navigator/app.dart @@ -37,43 +37,9 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - heroTag: 'btn1', - onPressed: () => createPost( - context, - timelineService, - timelineOptions, - getConfig(timelineService), - ), - 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( - configuration: getConfig( - timelineService, - ), - context: context), - ), + return timeLineNavigatorUserStory( + context: context, + configuration: getConfig(timelineService), ); } } diff --git a/packages/flutter_timeline/example/lib/config/config.dart b/packages/flutter_timeline/example/lib/config/config.dart index 9b54ccc..5b3b5ca 100644 --- a/packages/flutter_timeline/example/lib/config/config.dart +++ b/packages/flutter_timeline/example/lib/config/config.dart @@ -6,6 +6,7 @@ TimelineUserStoryConfiguration getConfig(TimelineService service) { service: service, userId: 'test_user', optionsBuilder: (context) => options, + enablePostOverviewScreen: false, ); } diff --git a/packages/flutter_timeline/lib/flutter_timeline.dart b/packages/flutter_timeline/lib/flutter_timeline.dart index 9c68eb6..f03d1f1 100644 --- a/packages/flutter_timeline/lib/flutter_timeline.dart +++ b/packages/flutter_timeline/lib/flutter_timeline.dart @@ -5,8 +5,8 @@ /// Flutter Timeline library library flutter_timeline; +export 'package:flutter_timeline/src/flutter_timeline_gorouter_userstory.dart'; 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'; export 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart new file mode 100644 index 0000000..e1f6cbf --- /dev/null +++ b/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; +import 'package:flutter_timeline/src/go_router.dart'; +import 'package:go_router/go_router.dart'; + +/// Retrieves a list of GoRouter routes for timeline stories. +/// +/// This function retrieves a list of GoRouter routes for displaying timeline +/// stories. It takes an optional [TimelineUserStoryConfiguration] as parameter. +/// If no configuration is provided, default values will be used. +List getTimelineStoryRoutes({ + TimelineUserStoryConfiguration? configuration, +}) { + var config = configuration ?? + TimelineUserStoryConfiguration( + userId: 'test_user', + service: TimelineService( + postService: LocalTimelinePostService(), + ), + optionsBuilder: (context) => const TimelineOptions(), + ); + + return [ + GoRoute( + path: TimelineUserStoryRoutes.timelineHome, + pageBuilder: (context, state) { + var timelineScreen = TimelineScreen( + userId: config.userId, + onUserTap: (user) => config.onUserTap?.call(context, user), + service: config.service, + options: config.optionsBuilder(context), + onPostTap: (post) async => + config.onPostTap?.call(context, post) ?? + await context.push( + TimelineUserStoryRoutes.timelineViewPath(post.id), + ), + filterEnabled: config.filterEnabled, + postWidgetBuilder: config.postWidgetBuilder, + ); + + return buildScreenWithoutTransition( + context: context, + state: state, + child: config.openPageBuilder?.call( + context, + timelineScreen, + ) ?? + Scaffold( + appBar: AppBar(), + body: timelineScreen, + floatingActionButton: FloatingActionButton( + onPressed: () async => context.go( + TimelineUserStoryRoutes.timelinePostCreation, + ), + child: const Icon(Icons.add), + ), + ), + ); + }, + ), + GoRoute( + path: TimelineUserStoryRoutes.timelineView, + pageBuilder: (context, state) { + var post = + config.service.postService.getPost(state.pathParameters['post']!)!; + + var timelinePostWidget = TimelinePostScreen( + userId: config.userId, + options: config.optionsBuilder(context), + service: config.service, + post: post, + onPostDelete: () => config.onPostDelete?.call(context, post), + onUserTap: (user) => config.onUserTap?.call(context, user), + ); + + return buildScreenWithoutTransition( + context: context, + state: state, + child: config.openPageBuilder?.call( + context, + timelinePostWidget, + ) ?? + Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () => + context.go(TimelineUserStoryRoutes.timelineHome), + ), + ), + body: timelinePostWidget, + ), + ); + }, + ), + GoRoute( + path: TimelineUserStoryRoutes.timelinePostCreation, + pageBuilder: (context, state) { + var timelinePostCreationWidget = TimelinePostCreationScreen( + userId: config.userId, + options: config.optionsBuilder(context), + service: config.service, + onPostCreated: (post) async { + await config.service.postService.createPost(post); + if (context.mounted) { + context.go(TimelineUserStoryRoutes.timelineHome); + } + }, + onPostOverview: (post) async => context.push( + TimelineUserStoryRoutes.timelinePostOverview, + extra: post, + ), + enablePostOverviewScreen: config.enablePostOverviewScreen, + ); + + return buildScreenWithoutTransition( + context: context, + state: state, + child: config.openPageBuilder?.call( + context, + timelinePostCreationWidget, + ) ?? + Scaffold( + appBar: AppBar( + title: Text( + config.optionsBuilder(context).translations.postCreation, + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () => + context.go(TimelineUserStoryRoutes.timelineHome), + ), + ), + body: timelinePostCreationWidget, + ), + ); + }, + ), + GoRoute( + path: TimelineUserStoryRoutes.timelinePostOverview, + pageBuilder: (context, state) { + var post = state.extra! as TimelinePost; + + var timelinePostOverviewWidget = TimelinePostOverviewScreen( + options: config.optionsBuilder(context), + service: config.service, + timelinePost: post, + onPostSubmit: (post) async { + await config.service.postService.createPost(post); + if (context.mounted) { + context.go(TimelineUserStoryRoutes.timelineHome); + } + }, + ); + + return buildScreenWithoutTransition( + context: context, + state: state, + child: config.openPageBuilder?.call( + context, + timelinePostOverviewWidget, + ) ?? + timelinePostOverviewWidget, + ); + }, + ), + ]; +} 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 44df32b..d89653a 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -44,26 +44,40 @@ Widget _timelineScreenRoute({ optionsBuilder: (context) => const TimelineOptions(), ); - return TimelineScreen( - service: config.service, - options: config.optionsBuilder(context), - userId: config.userId, - onPostTap: (post) async => - config.onPostTap?.call(context, post) ?? - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _postDetailScreenRoute( - configuration: config, - context: context, - post: post, - ), + return Scaffold( + appBar: AppBar(), + floatingActionButton: FloatingActionButton( + onPressed: () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _postCreationScreenRoute( + configuration: config, + context: context, ), ), - onUserTap: (userId) { - config.onUserTap?.call(context, userId); - }, - filterEnabled: config.filterEnabled, - postWidgetBuilder: config.postWidgetBuilder, + ), + child: const Icon(Icons.add), + ), + body: TimelineScreen( + service: config.service, + options: config.optionsBuilder(context), + userId: config.userId, + onPostTap: (post) async => + config.onPostTap?.call(context, post) ?? + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _postDetailScreenRoute( + configuration: config, + context: context, + post: post, + ), + ), + ), + onUserTap: (userId) { + config.onUserTap?.call(context, userId); + }, + filterEnabled: config.filterEnabled, + postWidgetBuilder: config.postWidgetBuilder, + ), ); } @@ -98,3 +112,98 @@ Widget _postDetailScreenRoute({ }, ); } + +/// A widget function that creates a post creation screen route. +/// +/// This function creates a route for displaying a post creation screen. It takes +/// a [BuildContext] and an optional [TimelineUserStoryConfiguration] as +/// parameters. If no configuration is provided, default values will be used. +Widget _postCreationScreenRoute({ + required BuildContext context, + TimelineUserStoryConfiguration? configuration, +}) { + var config = configuration ?? + TimelineUserStoryConfiguration( + userId: 'test_user', + service: TimelineService( + postService: LocalTimelinePostService(), + ), + optionsBuilder: (context) => const TimelineOptions(), + ); + + return Scaffold( + appBar: AppBar( + title: Text( + config.optionsBuilder(context).translations.postCreation, + ), + ), + body: TimelinePostCreationScreen( + userId: config.userId, + service: config.service, + options: config.optionsBuilder(context), + onPostCreated: (post) async { + await config.service.postService.createPost(post); + if (context.mounted) { + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + _timelineScreenRoute(configuration: config, context: context), + ), + ); + } + }, + onPostOverview: (post) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _postOverviewScreenRoute( + configuration: config, + context: context, + post: post, + ), + ), + ); + }, + enablePostOverviewScreen: config.enablePostOverviewScreen, + ), + ); +} + +/// A widget function that creates a post overview screen route. +/// +/// This function creates a route for displaying a post overview screen. It takes +/// a [BuildContext], a [TimelinePost], and an optional +/// [TimelineUserStoryConfiguration] as parameters. If no configuration is +/// provided, default values will be used. +Widget _postOverviewScreenRoute({ + required BuildContext context, + required TimelinePost post, + TimelineUserStoryConfiguration? configuration, +}) { + var config = configuration ?? + TimelineUserStoryConfiguration( + userId: 'test_user', + service: TimelineService( + postService: LocalTimelinePostService(), + ), + optionsBuilder: (context) => const TimelineOptions(), + ); + + return TimelinePostOverviewScreen( + timelinePost: post, + options: config.optionsBuilder(context), + service: config.service, + onPostSubmit: (post) async { + await config.service.postService.createPost(post); + if (context.mounted) { + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + _timelineScreenRoute(configuration: config, context: context), + ), + ); + } + }, + ); +} diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart deleted file mode 100644 index 0d35cd3..0000000 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import 'package:flutter/material.dart'; -import 'package:flutter_timeline/flutter_timeline.dart'; -import 'package:flutter_timeline/src/go_router.dart'; -import 'package:go_router/go_router.dart'; - -/// Retrieves a list of GoRouter routes for timeline stories. -/// -/// This function retrieves a list of GoRouter routes for displaying timeline -/// stories. It takes an optional [TimelineUserStoryConfiguration] as parameter. -/// If no configuration is provided, default values will be used. -List getTimelineStoryRoutes({ - TimelineUserStoryConfiguration? configuration, -}) { - var config = configuration ?? - TimelineUserStoryConfiguration( - userId: 'test_user', - service: TimelineService( - postService: LocalTimelinePostService(), - ), - optionsBuilder: (context) => const TimelineOptions(), - ); - - return [ - GoRoute( - path: TimelineUserStoryRoutes.timelineHome, - pageBuilder: (context, state) { - var timelineScreen = TimelineScreen( - userId: config.userId, - onUserTap: (user) => config.onUserTap?.call(context, user), - service: config.service, - options: config.optionsBuilder(context), - onPostTap: (post) async => - config.onPostTap?.call(context, post) ?? - await context.push( - TimelineUserStoryRoutes.timelineViewPath(post.id), - ), - filterEnabled: config.filterEnabled, - postWidgetBuilder: config.postWidgetBuilder, - ); - - return buildScreenWithoutTransition( - context: context, - state: state, - child: config.openPageBuilder?.call( - context, - timelineScreen, - ) ?? - Scaffold( - body: timelineScreen, - ), - ); - }, - ), - GoRoute( - path: TimelineUserStoryRoutes.timelineView, - pageBuilder: (context, state) { - var post = - config.service.postService.getPost(state.pathParameters['post']!)!; - - var timelinePostWidget = TimelinePostScreen( - userId: config.userId, - options: config.optionsBuilder(context), - service: config.service, - post: post, - onPostDelete: () => config.onPostDelete?.call(context, post), - onUserTap: (user) => config.onUserTap?.call(context, user), - ); - - return buildScreenWithoutTransition( - context: context, - state: state, - child: config.openPageBuilder?.call( - context, - timelinePostWidget, - ) ?? - Scaffold( - body: timelinePostWidget, - ), - ); - }, - ), - ]; -} diff --git a/packages/flutter_timeline/lib/src/routes.dart b/packages/flutter_timeline/lib/src/routes.dart index b6c70e5..9900ad5 100644 --- a/packages/flutter_timeline/lib/src/routes.dart +++ b/packages/flutter_timeline/lib/src/routes.dart @@ -6,4 +6,6 @@ mixin TimelineUserStoryRoutes { static const String timelineHome = '/timeline'; static const String timelineView = '/timeline-view/:post'; static String timelineViewPath(String postId) => '/timeline-view/$postId'; + static const String timelinePostCreation = '/timeline-post-creation'; + static String timelinePostOverview = '/timeline-post-overview'; } 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 df04d83..21d5388 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart @@ -31,6 +31,7 @@ class TimelineTranslations { required this.searchHint, required this.postOverview, required this.postIn, + required this.postCreation, }); const TimelineTranslations.empty() @@ -58,7 +59,8 @@ class TimelineTranslations { timelineSelectionDescription = 'Choose a category', searchHint = 'Search...', postOverview = 'Post Overview', - postIn = 'Post in'; + postIn = 'Post in', + postCreation = 'Create Post'; final String noPosts; final String noPostsWithFilter; @@ -89,6 +91,7 @@ class TimelineTranslations { final String postOverview; final String postIn; + final String postCreation; TimelineTranslations copyWith({ String? noPosts, @@ -115,6 +118,7 @@ class TimelineTranslations { String? searchHint, String? postOverview, String? postIn, + String? postCreation, }) => TimelineTranslations( noPosts: noPosts ?? this.noPosts, @@ -144,5 +148,6 @@ class TimelineTranslations { searchHint: searchHint ?? this.searchHint, postOverview: postOverview ?? this.postOverview, postIn: postIn ?? this.postIn, + postCreation: postCreation ?? this.postCreation, ); } 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 6d82ff0..48e8d3b 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 @@ -19,6 +19,7 @@ class TimelinePostCreationScreen extends StatefulWidget { required this.options, this.postCategory, this.onPostOverview, + this.enablePostOverviewScreen = false, super.key, }); @@ -37,6 +38,7 @@ class TimelinePostCreationScreen extends StatefulWidget { /// Nullable callback for routing to the post overview final void Function(TimelinePost)? onPostOverview; + final bool enablePostOverviewScreen; @override State createState() => @@ -107,11 +109,10 @@ class _TimelinePostCreationScreenState image: image, ); - if (widget.onPostOverview != null) { + if (widget.enablePostOverviewScreen) { widget.onPostOverview?.call(post); } else { - var newPost = await widget.service.postService.createPost(post); - widget.onPostCreated.call(newPost); + widget.onPostCreated.call(post); } } @@ -287,7 +288,9 @@ class _TimelinePostCreationScreenState } : null, child: Text( - widget.options.translations.checkPost, + widget.enablePostOverviewScreen + ? widget.options.translations.checkPost + : widget.options.translations.postCreation, style: theme.textTheme.bodyMedium, ), ), 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 9bd5a82..ca812f5 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -78,7 +78,10 @@ class _TimelineScreenState extends State { void initState() { super.initState(); controller = widget.scrollController ?? ScrollController(); - unawaited(loadPosts()); + // only load the posts after the first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(loadPosts()); + }); } @override @@ -91,6 +94,7 @@ class _TimelineScreenState extends State { return ListenableBuilder( listenable: service.postService, builder: (context, _) { + if (!context.mounted) return const SizedBox(); var posts = widget.posts ?? service.postService.getPosts(category); if (widget.filterEnabled && filterWord != null) { @@ -271,7 +275,7 @@ class _TimelineScreenState extends State { } Future loadPosts() async { - if (widget.posts != null) return; + if (widget.posts != null || !context.mounted) return; try { await service.postService.fetchPosts(category); setState(() {