diff --git a/packages/flutter_timeline/example/lib/config/config.dart b/packages/flutter_timeline/example/lib/config/config.dart index 046615f..734c1eb 100644 --- a/packages/flutter_timeline/example/lib/config/config.dart +++ b/packages/flutter_timeline/example/lib/config/config.dart @@ -14,23 +14,25 @@ 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(), - ), - ], + categoriesOptions: CategoriesOptions( + 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, 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 cc375aa..468b521 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -30,6 +30,8 @@ Widget _timelineScreenRoute( onUserTap: (userId) { configuration.onUserTap?.call(context, userId); }, + filterEnabled: configuration.filterEnabled, + postWidgetBuilder: configuration.postWidgetBuilder, ); Widget _postDetailScreenRoute( diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index 347fcd2..cdd4ba5 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -26,6 +26,8 @@ List getTimelineStoryRoutes( await context.push( TimelineUserStoryRoutes.timelineViewPath(post.id), ), + filterEnabled: configuration.filterEnabled, + postWidgetBuilder: configuration.postWidgetBuilder, ); return buildScreenWithoutTransition( diff --git a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart index c06ce3f..8708d3b 100644 --- a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart +++ b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart @@ -17,6 +17,8 @@ class TimelineUserStoryConfiguration { this.onPostTap, this.onUserTap, this.onPostDelete, + this.filterEnabled = false, + this.postWidgetBuilder, }); final String userId; @@ -34,4 +36,8 @@ class TimelineUserStoryConfiguration { final Function(BuildContext context, TimelinePost post)? onPostTap; final Widget Function(BuildContext context, TimelinePost post)? onPostDelete; + + final bool filterEnabled; + + final Widget Function(TimelinePost post)? postWidgetBuilder; } diff --git a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart index 5fc676f..733c73e 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2023 Iconica // // SPDX-License-Identifier: BSD-3-Clause +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_image_picker/flutter_image_picker.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; @@ -9,7 +10,7 @@ import 'package:flutter_timeline_view/src/config/timeline_translations.dart'; import 'package:intl/intl.dart'; class TimelineOptions { - TimelineOptions({ + const TimelineOptions({ this.theme = const TimelineTheme(), this.translations = const TimelineTranslations.empty(), this.imagePickerConfig = const ImagePickerConfig(), @@ -40,13 +41,8 @@ class TimelineOptions { this.iconSize = 26, this.postWidgetheight, this.postPadding = const EdgeInsets.all(12.0), - this.categoriesBuilder, - this.categoryButtonBuilder, - this.categorySelectorHorizontalPadding, - this.filterEnabled = false, - this.initialFilterWord, - this.searchBarBuilder, - this.postWidget, + this.filterOptions = const FilterOptions(), + this.categoriesOptions = const CategoriesOptions(), }); /// Theming options for the timeline @@ -120,6 +116,18 @@ class TimelineOptions { /// Padding of each post final EdgeInsets postPadding; + final FilterOptions filterOptions; + + final CategoriesOptions categoriesOptions; +} + +class CategoriesOptions { + const CategoriesOptions({ + this.categoriesBuilder, + this.categoryButtonBuilder, + this.categorySelectorHorizontalPadding, + }); + /// List of categories that the user can select. /// If this is null no categories will be shown. final List Function(BuildContext context)? @@ -136,22 +144,39 @@ class TimelineOptions { /// Overides the standard horizontal padding of the whole category selector. final double? categorySelectorHorizontalPadding; - /// if true the filter textfield is enabled. - bool filterEnabled; + TimelineCategory? getCategoryByKey( + BuildContext context, + String? key, + ) { + if (categoriesBuilder == null) { + return null; + } + + return categoriesBuilder! + .call(context) + .firstWhereOrNull((category) => category.key == key); + } +} + +class FilterOptions { + const FilterOptions({ + this.initialFilterWord, + this.searchBarBuilder, + this.onFilterEnabledChange, + }); /// Set a value to search through posts. When set the searchbar is shown. /// If null no searchbar is shown. final String? initialFilterWord; + // Possibilty to override the standard search bar. final Widget Function( Future> Function( String filterWord, - Map options, ) search, )? searchBarBuilder; - /// Override the standard postwidget - final Widget Function(TimelinePost post)? postWidget; + final void Function({required bool filterEnabled})? onFilterEnabledChange; } typedef ButtonBuilder = Widget Function( 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 9ed7db4..d367d87 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -18,6 +18,8 @@ class TimelineScreen extends StatefulWidget { this.onUserTap, this.posts, this.timelineCategory, + this.postWidgetBuilder, + this.filterEnabled = false, super.key, }); @@ -46,21 +48,28 @@ class TimelineScreen extends StatefulWidget { /// 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)? postWidgetBuilder; + + /// if true the filter textfield is enabled. + final bool filterEnabled; + @override State createState() => _TimelineScreenState(); } class _TimelineScreenState extends State { late ScrollController controller; - late var textFieldController = - TextEditingController(text: widget.options.initialFilterWord); + late var textFieldController = TextEditingController( + text: widget.options.filterOptions.initialFilterWord, + ); late var service = widget.service; bool isLoading = true; late var category = widget.timelineCategory; - late var filterWord = widget.options.initialFilterWord; + late var filterWord = widget.options.filterOptions.initialFilterWord; @override void initState() { @@ -81,13 +90,7 @@ class _TimelineScreenState extends State { builder: (context, _) { var posts = widget.posts ?? service.getPosts(category); - posts = posts - .where( - (p) => category == null || p.category == category, - ) - .toList(); - - if (widget.options.filterEnabled && filterWord != null) { + if (widget.filterEnabled && filterWord != null) { if (service is TimelineFilterService?) { posts = (service as TimelineFilterService).filterPosts(filterWord!, {}); @@ -97,6 +100,12 @@ class _TimelineScreenState extends State { } } + posts = posts + .where( + (p) => category == null || p.category == category, + ) + .toList(); + // sort posts by date if (widget.options.sortPostsAscending != null) { posts.sort( @@ -112,7 +121,7 @@ class _TimelineScreenState extends State { SizedBox( height: widget.options.padding.top, ), - if (widget.options.filterEnabled) ...[ + if (widget.filterEnabled) ...[ Padding( padding: EdgeInsets.symmetric( horizontal: widget.options.padding.horizontal, @@ -151,8 +160,9 @@ class _TimelineScreenState extends State { onTap: () { setState(() { textFieldController.clear(); - widget.options.filterEnabled = false; filterWord = null; + widget.options.filterOptions.onFilterEnabledChange + ?.call(filterEnabled: false); }); }, child: const Padding( @@ -191,7 +201,7 @@ class _TimelineScreenState extends State { ...posts.map( (post) => Padding( padding: widget.options.postPadding, - child: widget.options.postWidget?.call(post) ?? + child: widget.postWidgetBuilder?.call(post) ?? TimelinePostWidget( service: widget.service, userId: widget.userId, diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart index f0d3cfc..858d0b9 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart @@ -17,22 +17,23 @@ class CategorySelector extends StatelessWidget { @override Widget build(BuildContext context) { - if (options.categoriesBuilder == null) { + if (options.categoriesOptions.categoriesBuilder == null) { return const SizedBox.shrink(); } - var categories = options.categoriesBuilder!(context); + var categories = options.categoriesOptions.categoriesBuilder!(context); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ SizedBox( - width: options.categorySelectorHorizontalPadding ?? - max(options.padding.horizontal - 4, 0), + width: + options.categoriesOptions.categorySelectorHorizontalPadding ?? + max(options.padding.horizontal - 4, 0), ), for (var category in categories) ...[ - options.categoryButtonBuilder?.call( + options.categoriesOptions.categoryButtonBuilder?.call( categoryKey: category.key, categoryName: category.title, onTap: () => onTapCategory(category.key), @@ -48,8 +49,9 @@ class CategorySelector extends StatelessWidget { ), ], SizedBox( - width: options.categorySelectorHorizontalPadding ?? - max(options.padding.horizontal - 4, 0), + width: + options.categoriesOptions.categorySelectorHorizontalPadding ?? + max(options.padding.horizontal - 4, 0), ), ], ), diff --git a/packages/flutter_timeline_view/pubspec.yaml b/packages/flutter_timeline_view/pubspec.yaml index 74f004f..19fae1e 100644 --- a/packages/flutter_timeline_view/pubspec.yaml +++ b/packages/flutter_timeline_view/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_image_picker ref: 1.0.4 + collection: any dev_dependencies: flutter_lints: ^2.0.0