feat: added go router and navigator user stories

This commit is contained in:
Jacques 2024-01-24 16:43:49 +01:00
parent 9125c47ac4
commit e99e81c907
24 changed files with 437 additions and 320 deletions

View file

@ -54,7 +54,7 @@ final GoRouter _router = GoRouter(
); );
}, },
), ),
...getTimelineStoryRoutes() ...getTimelineStoryRoutes(timelineUserStoryConfiguration)
], ],
); );
``` ```

View file

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

View file

@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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),
),
);
}
}

View file

@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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();
},
),
),
),
);
},
),
),
);
}
}

View file

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

View file

@ -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/material.dart';
import 'package:flutter_timeline/flutter_timeline.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
void main() { void main() {
initializeDateFormatting(); initializeDateFormatting();
runApp(const MyApp()); // Uncomment any, but only one, of these lines to run the example with specific navigation.
}
runApp(const WidgetApp());
class MyApp extends StatelessWidget { // runApp(const NavigatorApp());
const MyApp({super.key}); // runApp(const GoRouterApp());
@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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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,
),
);
}
} }

View file

@ -72,7 +72,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
@override @override
Future<List<TimelinePost>> fetchPosts(String? category) async { Future<List<TimelinePost>> fetchPosts(String? category) async {
var posts = getMockedPosts(); posts = getMockedPosts();
notifyListeners(); notifyListeners();
return posts; return posts;
} }
@ -175,7 +175,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
id: 'Post0', id: 'Post0',
creatorId: 'test_user', creatorId: 'test_user',
title: 'Post 0', title: 'Post 0',
category: 'text', category: null,
content: "Post 0 content", content: "Post 0 content",
likes: 0, likes: 0,
reaction: 0, reaction: 0,

View file

@ -38,6 +38,7 @@ dependencies:
flutter_timeline: flutter_timeline:
path: ../ path: ../
intl: ^0.19.0 intl: ^0.19.0
go_router: ^13.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

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

View file

@ -5,6 +5,7 @@
/// Flutter Timeline library /// Flutter Timeline library
library flutter_timeline; 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/flutter_timeline_userstory.dart';
export 'package:flutter_timeline/src/models/timeline_configuration.dart'; export 'package:flutter_timeline/src/models/timeline_configuration.dart';
export 'package:flutter_timeline/src/routes.dart'; export 'package:flutter_timeline/src/routes.dart';

View file

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

View file

@ -22,12 +22,16 @@ List<GoRoute> getTimelineStoryRoutes(
service: configuration.service, service: configuration.service,
options: configuration.optionsBuilder(context), options: configuration.optionsBuilder(context),
onPostTap: (post) async => onPostTap: (post) async =>
TimelineUserStoryRoutes.timelineViewPath(post.id), configuration.onPostTap?.call(context, post) ??
await context.push(
TimelineUserStoryRoutes.timelineViewPath(post.id),
),
); );
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,
state: state, state: state,
child: configuration.mainPageBuilder?.call( child: configuration.openPageBuilder?.call(
context, context,
timelineScreen, timelineScreen,
) ?? ) ??
@ -37,73 +41,27 @@ List<GoRoute> 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( GoRoute(
path: TimelineUserStoryRoutes.timelineView, path: TimelineUserStoryRoutes.timelineView,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var post =
configuration.service.getPost(state.pathParameters['post']!)!;
var timelinePostWidget = TimelinePostScreen( var timelinePostWidget = TimelinePostScreen(
userId: configuration.userId, userId: configuration.userId,
options: configuration.optionsBuilder(context), options: configuration.optionsBuilder(context),
service: configuration.service, service: configuration.service,
post: configuration.service.getPost(state.pathParameters['post']!)!, post: post,
onPostDelete: () => context.pop(), onPostDelete: () => configuration.onPostDelete?.call(context, post),
onUserTap: (user) => configuration.onUserTap?.call(context, user), onUserTap: (user) => configuration.onUserTap?.call(context, user),
); );
var category = configuration.categoriesBuilder(context).first;
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,
state: state, state: state,
child: configuration.postScreenBuilder?.call( child: configuration.openPageBuilder?.call(
context, context,
timelinePostWidget, timelinePostWidget,
category,
) ?? ) ??
Scaffold( Scaffold(
body: timelinePostWidget, body: timelinePostWidget,

View file

@ -9,42 +9,29 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart';
@immutable @immutable
class TimelineUserStoryConfiguration { class TimelineUserStoryConfiguration {
const TimelineUserStoryConfiguration({ const TimelineUserStoryConfiguration({
required this.categoriesBuilder,
required this.optionsBuilder,
required this.userId, required this.userId,
required this.service, required this.service,
required this.userService, required this.userService,
this.mainPageBuilder, required this.optionsBuilder,
this.postScreenBuilder, this.openPageBuilder,
this.postCreationScreenBuilder, this.onPostTap,
this.postSelectionScreenBuilder,
this.onUserTap, this.onUserTap,
this.onPostDelete,
}); });
final String userId; 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 TimelineService service;
final TimelineUserService userService; final TimelineUserService userService;
final TimelineOptions Function(BuildContext context) optionsBuilder; final TimelineOptions Function(BuildContext context) optionsBuilder;
final List<TimelineCategory> 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;
} }

View file

@ -4,10 +4,6 @@
mixin TimelineUserStoryRoutes { mixin TimelineUserStoryRoutes {
static const String timelineHome = '/timeline'; 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 const String timelineView = '/timeline-view/:post';
static String timelineViewPath(String postId) => '/timeline-view/$postId'; static String timelineViewPath(String postId) => '/timeline-view/$postId';
} }

View file

@ -3,13 +3,13 @@ import 'package:flutter/material.dart';
@immutable @immutable
class TimelineCategory { class TimelineCategory {
const TimelineCategory({ const TimelineCategory({
required this.name, required this.key,
required this.title, required this.title,
required this.icon, required this.icon,
this.canCreate = true, this.canCreate = true,
this.canView = true, this.canView = true,
}); });
final String name; final String? key;
final String title; final String title;
final Widget icon; final Widget icon;
final bool canCreate; final bool canCreate;

View file

@ -15,12 +15,12 @@ class TimelinePost {
required this.id, required this.id,
required this.creatorId, required this.creatorId,
required this.title, required this.title,
required this.category,
required this.content, required this.content,
required this.likes, required this.likes,
required this.reaction, required this.reaction,
required this.createdAt, required this.createdAt,
required this.reactionEnabled, required this.reactionEnabled,
this.category,
this.creator, this.creator,
this.likedBy, this.likedBy,
this.reactions, this.reactions,
@ -67,7 +67,7 @@ class TimelinePost {
final String title; final String title;
/// The category of the post on which can be filtered. /// The category of the post on which can be filtered.
final String category; final String? category;
/// The url of the image of the post. /// The url of the image of the post.
final String? imageUrl; final String? imageUrl;

View file

@ -12,4 +12,6 @@ export 'src/screens/timeline_post_creation_screen.dart';
export 'src/screens/timeline_post_screen.dart'; export 'src/screens/timeline_post_screen.dart';
export 'src/screens/timeline_screen.dart'; export 'src/screens/timeline_screen.dart';
export 'src/screens/timeline_selection_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'; export 'src/widgets/timeline_post_widget.dart';

View file

@ -40,13 +40,13 @@ class TimelineOptions {
this.iconSize = 26, this.iconSize = 26,
this.postWidgetheight, this.postWidgetheight,
this.postPadding = const EdgeInsets.all(12.0), this.postPadding = const EdgeInsets.all(12.0),
this.categories, this.categoriesBuilder,
this.categoryButtonBuilder, this.categoryButtonBuilder,
this.catergoryLabelBuilder,
this.categorySelectorHorizontalPadding, this.categorySelectorHorizontalPadding,
this.filterEnabled = false, this.filterEnabled = false,
this.initialFilterWord, this.initialFilterWord,
this.searchBarBuilder, this.searchBarBuilder,
this.postWidget,
}); });
/// Theming options for the timeline /// Theming options for the timeline
@ -122,7 +122,8 @@ class TimelineOptions {
/// List of categories that the user can select. /// List of categories that the user can select.
/// If this is null no categories will be shown. /// If this is null no categories will be shown.
final List<String>? categories; final List<TimelineCategory> Function(BuildContext context)?
categoriesBuilder;
/// Abilty to override the standard category selector /// Abilty to override the standard category selector
final Widget Function({ final Widget Function({
@ -132,10 +133,6 @@ class TimelineOptions {
required bool selected, required bool selected,
})? categoryButtonBuilder; })? 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. /// Overides the standard horizontal padding of the whole category selector.
final double? categorySelectorHorizontalPadding; final double? categorySelectorHorizontalPadding;
@ -152,6 +149,9 @@ class TimelineOptions {
Map<String, dynamic> options, Map<String, dynamic> options,
) search, ) search,
)? searchBarBuilder; )? searchBarBuilder;
/// Override the standard postwidget
final Widget Function(TimelinePost post)? postWidget;
} }
typedef ButtonBuilder = Widget Function( typedef ButtonBuilder = Widget Function(

View file

@ -13,16 +13,16 @@ import 'package:flutter_timeline_view/src/config/timeline_options.dart';
class TimelinePostCreationScreen extends StatefulWidget { class TimelinePostCreationScreen extends StatefulWidget {
const TimelinePostCreationScreen({ const TimelinePostCreationScreen({
required this.userId, required this.userId,
required this.postCategory,
required this.onPostCreated, required this.onPostCreated,
required this.service, required this.service,
required this.options, required this.options,
this.postCategory,
super.key, super.key,
}); });
final String userId; final String userId;
final String postCategory; final String? postCategory;
/// called when the post is created /// called when the post is created
final Function(TimelinePost) onPostCreated; final Function(TimelinePost) onPostCreated;

View file

@ -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:flutter_timeline_view/src/widgets/tappable_image.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class TimelinePostScreen extends StatefulWidget { class TimelinePostScreen extends StatelessWidget {
const TimelinePostScreen({ const TimelinePostScreen({
required this.userId, required this.userId,
required this.service, required this.service,
@ -44,10 +44,45 @@ class TimelinePostScreen extends StatefulWidget {
final VoidCallback onPostDelete; final VoidCallback onPostDelete;
@override @override
State<TimelinePostScreen> 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<TimelinePostScreen> { 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; TimelinePost? post;
bool isLoading = true; bool isLoading = true;
@ -96,8 +131,9 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
var dateFormat = widget.options.dateFormat ?? var dateFormat = widget.options.dateFormat ??
DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode); DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode);
var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm'); var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm');
if (isLoading) { if (isLoading) {
return const Center( const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
@ -185,12 +221,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
if (widget.options.allowAllDeletion || if (widget.options.allowAllDeletion ||
post.creator?.userId == widget.userId) post.creator?.userId == widget.userId)
PopupMenuButton( PopupMenuButton(
onSelected: (value) async { onSelected: (value) => widget.onPostDelete(),
if (value == 'delete') {
await widget.service.deletePost(post);
widget.onPostDelete();
}
},
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[ <PopupMenuEntry<String>>[
PopupMenuItem<String>( PopupMenuItem<String>(

View file

@ -7,19 +7,17 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
import 'package:flutter_timeline_view/flutter_timeline_view.dart'; import 'package:flutter_timeline_view/flutter_timeline_view.dart';
import 'package:flutter_timeline_view/src/widgets/category_selector.dart';
class TimelineScreen extends StatefulWidget { class TimelineScreen extends StatefulWidget {
const TimelineScreen({ const TimelineScreen({
required this.userId, required this.userId,
required this.service, required this.service,
required this.options, required this.options,
required this.onPostTap,
this.scrollController, this.scrollController,
this.onPostTap,
this.onUserTap, this.onUserTap,
this.posts, this.posts,
this.timelineCategory, this.timelineCategory,
this.postWidget,
super.key, super.key,
}); });
@ -43,14 +41,11 @@ class TimelineScreen extends StatefulWidget {
final List<TimelinePost>? posts; final List<TimelinePost>? posts;
/// Called when a post is tapped /// 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 /// If this is not null, the user can tap on the user avatar or name
final Function(String userId)? onUserTap; final Function(String userId)? onUserTap;
/// Override the standard postwidget
final Widget Function(TimelinePost post)? postWidget;
@override @override
State<TimelineScreen> createState() => _TimelineScreenState(); State<TimelineScreen> createState() => _TimelineScreenState();
} }
@ -196,35 +191,13 @@ class _TimelineScreenState extends State<TimelineScreen> {
...posts.map( ...posts.map(
(post) => Padding( (post) => Padding(
padding: widget.options.postPadding, padding: widget.options.postPadding,
child: widget.postWidget?.call(post) ?? child: widget.options.postWidget?.call(post) ??
TimelinePostWidget( TimelinePostWidget(
service: widget.service, service: widget.service,
userId: widget.userId, userId: widget.userId,
options: widget.options, options: widget.options,
post: post, post: post,
onTap: () async { onTap: () => widget.onPostTap(post),
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);
},
),
),
),
);
},
onTapLike: () async => onTapLike: () async =>
service.likePost(widget.userId, post), service.likePost(widget.userId, post),
onTapUnlike: () async => onTapUnlike: () async =>

View file

@ -2,7 +2,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline_view/flutter_timeline_view.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 { class CategorySelector extends StatelessWidget {
const CategorySelector({ const CategorySelector({
@ -18,10 +17,12 @@ class CategorySelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (options.categories == null) { if (options.categoriesBuilder == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
var categories = options.categoriesBuilder!(context);
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@ -30,37 +31,19 @@ class CategorySelector extends StatelessWidget {
width: options.categorySelectorHorizontalPadding ?? width: options.categorySelectorHorizontalPadding ??
max(options.padding.horizontal - 4, 0), max(options.padding.horizontal - 4, 0),
), ),
options.categoryButtonBuilder?.call( for (var category in categories) ...[
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!) ...[
options.categoryButtonBuilder?.call( options.categoryButtonBuilder?.call(
categoryKey: category, categoryKey: category.key,
categoryName: categoryName: category.title,
options.catergoryLabelBuilder?.call(category) ?? category, onTap: () => onTapCategory(category.key),
onTap: () => onTapCategory(category), selected: filter == category.key,
selected: filter == category,
) ?? ) ??
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: CategorySelectorButton( child: CategorySelectorButton(
category: category, category: category,
selected: filter == category, selected: filter == category.key,
onTap: () => onTapCategory(category), onTap: () => onTapCategory(category.key),
labelBuilder: options.catergoryLabelBuilder,
), ),
), ),
], ],

View file

@ -1,17 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
class CategorySelectorButton extends StatelessWidget { class CategorySelectorButton extends StatelessWidget {
const CategorySelectorButton({ const CategorySelectorButton({
required this.category, required this.category,
required this.selected, required this.selected,
required this.onTap, required this.onTap,
this.labelBuilder,
super.key, super.key,
}); });
final String? category; final TimelineCategory category;
final bool selected; final bool selected;
final String Function(String? category)? labelBuilder;
final void Function() onTap; final void Function() onTap;
@override @override
@ -41,7 +40,7 @@ class CategorySelectorButton extends StatelessWidget {
), ),
), ),
child: Text( child: Text(
labelBuilder?.call(category) ?? category ?? 'All', category.title,
style: theme.textTheme.labelMedium?.copyWith( style: theme.textTheme.labelMedium?.copyWith(
color: selected color: selected
? theme.colorScheme.onPrimary ? theme.colorScheme.onPrimary