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_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<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,
),
);
}
// Uncomment any, but only one, of these lines to run the example with specific navigation.
runApp(const WidgetApp());
// runApp(const NavigatorApp());
// runApp(const GoRouterApp());
}

View file

@ -72,7 +72,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
@override
Future<List<TimelinePost>> 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,

View file

@ -38,6 +38,7 @@ dependencies:
flutter_timeline:
path: ../
intl: ^0.19.0
go_router: ^13.0.1
dev_dependencies:
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
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';

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,
options: configuration.optionsBuilder(context),
onPostTap: (post) async =>
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<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(
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,

View file

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

View file

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

View file

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

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

View file

@ -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<String>? categories;
final List<TimelineCategory> 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<String, dynamic> options,
) search,
)? searchBarBuilder;
/// Override the standard postwidget
final Widget Function(TimelinePost post)? postWidget;
}
typedef ButtonBuilder = Widget Function(

View file

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

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: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<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;
bool isLoading = true;
@ -96,8 +131,9 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
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<TimelinePostScreen> {
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) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(

View file

@ -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<TimelinePost>? 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<TimelineScreen> createState() => _TimelineScreenState();
}
@ -196,35 +191,13 @@ class _TimelineScreenState extends State<TimelineScreen> {
...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 =>

View file

@ -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),
),
for (var category in categories) ...[
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!) ...[
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),
),
),
],

View file

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