feat(user-stories): add all routes to gorouter and navigator user stories

This commit is contained in:
FahadFahim71 2024-03-05 15:27:09 +01:00
parent 898583d1d1
commit 07f5872ec6
10 changed files with 325 additions and 150 deletions

View file

@ -37,43 +37,9 @@ class _MyHomePageState extends State<MyHomePage> {
@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),
);
}
}

View file

@ -6,6 +6,7 @@ TimelineUserStoryConfiguration getConfig(TimelineService service) {
service: service,
userId: 'test_user',
optionsBuilder: (context) => options,
enablePostOverviewScreen: false,
);
}

View file

@ -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';

View file

@ -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<GoRoute> getTimelineStoryRoutes({
TimelineUserStoryConfiguration? configuration,
}) {
var config = configuration ??
TimelineUserStoryConfiguration(
userId: 'test_user',
service: TimelineService(
postService: LocalTimelinePostService(),
),
optionsBuilder: (context) => const TimelineOptions(),
);
return <GoRoute>[
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,
);
},
),
];
}

View file

@ -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),
),
);
}
},
);
}

View file

@ -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<GoRoute> getTimelineStoryRoutes({
TimelineUserStoryConfiguration? configuration,
}) {
var config = configuration ??
TimelineUserStoryConfiguration(
userId: 'test_user',
service: TimelineService(
postService: LocalTimelinePostService(),
),
optionsBuilder: (context) => const TimelineOptions(),
);
return <GoRoute>[
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,
),
);
},
),
];
}

View file

@ -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';
}

View file

@ -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,
);
}

View file

@ -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<TimelinePostCreationScreen> 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,
),
),

View file

@ -78,7 +78,10 @@ class _TimelineScreenState extends State<TimelineScreen> {
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<TimelineScreen> {
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<TimelineScreen> {
}
Future<void> loadPosts() async {
if (widget.posts != null) return;
if (widget.posts != null || !context.mounted) return;
try {
await service.postService.fetchPosts(category);
setState(() {