From 7a2561ba2e2b213012be7710adb6853849faeea9 Mon Sep 17 00:00:00 2001 From: Jacques Date: Mon, 15 Jan 2024 14:23:25 +0100 Subject: [PATCH 01/11] fix(example): Add proper example with most basic functionality --- .../example/.gitignore | 1 - .../example/analysis_options.yaml | 0 .../flutter_timeline/example/lib/main.dart | 93 +++++++++ .../example/lib/post_screen.dart | 51 +++++ .../example/lib/timeline_service.dart | 186 ++++++++++++++++++ .../flutter_timeline/example/pubspec.yaml | 95 +++++++++ .../example/test/widget_test.dart | 30 +++ .../example/lib/main.dart | 49 ----- .../example/pubspec.yaml | 21 -- .../example/test/widget_test.dart | 14 -- 10 files changed, 455 insertions(+), 85 deletions(-) rename packages/{flutter_timeline_view => flutter_timeline}/example/.gitignore (98%) rename packages/{flutter_timeline_view => flutter_timeline}/example/analysis_options.yaml (100%) create mode 100644 packages/flutter_timeline/example/lib/main.dart create mode 100644 packages/flutter_timeline/example/lib/post_screen.dart create mode 100644 packages/flutter_timeline/example/lib/timeline_service.dart create mode 100644 packages/flutter_timeline/example/pubspec.yaml create mode 100644 packages/flutter_timeline/example/test/widget_test.dart delete mode 100644 packages/flutter_timeline_view/example/lib/main.dart delete mode 100644 packages/flutter_timeline_view/example/pubspec.yaml delete mode 100644 packages/flutter_timeline_view/example/test/widget_test.dart diff --git a/packages/flutter_timeline_view/example/.gitignore b/packages/flutter_timeline/example/.gitignore similarity index 98% rename from packages/flutter_timeline_view/example/.gitignore rename to packages/flutter_timeline/example/.gitignore index 24476c5..29a3a50 100644 --- a/packages/flutter_timeline_view/example/.gitignore +++ b/packages/flutter_timeline/example/.gitignore @@ -27,7 +27,6 @@ migrate_working_dir/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.packages .pub-cache/ .pub/ /build/ diff --git a/packages/flutter_timeline_view/example/analysis_options.yaml b/packages/flutter_timeline/example/analysis_options.yaml similarity index 100% rename from packages/flutter_timeline_view/example/analysis_options.yaml rename to packages/flutter_timeline/example/analysis_options.yaml diff --git a/packages/flutter_timeline/example/lib/main.dart b/packages/flutter_timeline/example/lib/main.dart new file mode 100644 index 0000000..f3d1e3c --- /dev/null +++ b/packages/flutter_timeline/example/lib/main.dart @@ -0,0 +1,93 @@ +import 'package:example/post_screen.dart'; +import 'package:example/timeline_service.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), + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + var timelineService = TestTimelineService(); + + @override + Widget build(BuildContext context) { + print('test'); + return Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { + createPost(); + }, + child: const Icon( + Icons.add, + color: Colors.white, + ), + ), + body: SafeArea( + child: TimelineScreen( + userId: 'test_id', + options: const TimelineOptions(), + onPostTap: (post) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PostScreen( + service: timelineService, + post: post, + ), + ), + ); + }, + service: timelineService, + ), + ), + ); + } + + void createPost() { + print('creating post'); + 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: false, + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/post_screen.dart b/packages/flutter_timeline/example/lib/post_screen.dart new file mode 100644 index 0000000..21d9b77 --- /dev/null +++ b/packages/flutter_timeline/example/lib/post_screen.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; + +class PostScreen extends StatefulWidget { + const PostScreen({ + required this.service, + required this.post, + super.key, + }); + + final TimelineService service; + final TimelinePost post; + + @override + State createState() => _PostScreenState(); +} + +class _PostScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: TimelinePostScreen( + userId: 'test_user', + service: widget.service, + userService: TestUserService(), + options: const TimelineOptions(), + post: widget.post, + onPostDelete: () { + print('delete post'); + }, + ), + ); + } +} + +class TestUserService implements TimelineUserService { + final Map _users = { + 'test_user': const TimelinePosterUserModel(userId: 'test_user') + }; + + @override + Future getUser(String userId) async { + if (_users.containsKey(userId)) { + return _users[userId]!; + } + + _users[userId] = TimelinePosterUserModel(userId: userId); + + return TimelinePosterUserModel(userId: userId); + } +} diff --git a/packages/flutter_timeline/example/lib/timeline_service.dart b/packages/flutter_timeline/example/lib/timeline_service.dart new file mode 100644 index 0000000..9e3e539 --- /dev/null +++ b/packages/flutter_timeline/example/lib/timeline_service.dart @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; + +// ignore: depend_on_referenced_packages +import 'package:uuid/uuid.dart'; + +class TestTimelineService with ChangeNotifier implements TimelineService { + List _posts = []; + + @override + Future createPost(TimelinePost post) async { + _posts.add(post); + notifyListeners(); + return post; + } + + @override + Future deletePost(TimelinePost post) async { + _posts = _posts.where((element) => element.id != post.id).toList(); + + notifyListeners(); + } + + @override + Future deletePostReaction( + TimelinePost post, + String reactionId, + ) async { + if (post.reactions != null && post.reactions!.isNotEmpty) { + var reaction = + post.reactions!.firstWhere((element) => element.id == reactionId); + var updatedPost = post.copyWith( + reaction: post.reaction - 1, + reactions: (post.reactions ?? [])..remove(reaction), + ); + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + + notifyListeners(); + return updatedPost; + } + return post; + } + + @override + Future fetchPostDetails(TimelinePost post) async { + var reactions = post.reactions ?? []; + var updatedReactions = []; + for (var reaction in reactions) { + updatedReactions.add(reaction.copyWith( + creator: const TimelinePosterUserModel(userId: 'test_user'))); + } + var updatedPost = post.copyWith(reactions: updatedReactions); + _posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); + notifyListeners(); + return updatedPost; + } + + @override + Future> fetchPosts(String? category) async { + print('fetch posts'); + var posts = getMockedPosts(); + _posts = posts; + notifyListeners(); + return posts; + } + + @override + Future> fetchPostsPaginated( + String? category, + int limit, + ) async { + notifyListeners(); + return _posts; + } + + @override + Future fetchPost(TimelinePost post) async { + notifyListeners(); + return post; + } + + @override + Future> refreshPosts(String? category) async { + var posts = []; + + _posts = [...posts, ..._posts]; + notifyListeners(); + return posts; + } + + @override + TimelinePost? getPost(String postId) => + (_posts.any((element) => element.id == postId)) + ? _posts.firstWhere((element) => element.id == postId) + : null; + + @override + List getPosts(String? category) => _posts + .where((element) => category == null || element.category == category) + .toList(); + + @override + Future likePost(String userId, TimelinePost post) async { + var updatedPost = post.copyWith( + likes: post.likes + 1, + likedBy: post.likedBy?..add(userId), + ); + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + + notifyListeners(); + return updatedPost; + } + + @override + Future unlikePost(String userId, TimelinePost post) async { + var updatedPost = post.copyWith( + likes: post.likes - 1, + likedBy: post.likedBy?..remove(userId), + ); + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + + notifyListeners(); + return updatedPost; + } + + @override + Future reactToPost( + TimelinePost post, + TimelinePostReaction reaction, { + Uint8List? image, + }) async { + var reactionId = const Uuid().v4(); + var updatedReaction = reaction.copyWith( + id: reactionId, + creator: const TimelinePosterUserModel(userId: 'test_user')); + + + var updatedPost = post.copyWith( + reaction: post.reaction + 1, + reactions: post.reactions?..add(updatedReaction), + ); + + + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + notifyListeners(); + return updatedPost; + } + + List getMockedPosts() { + return [ + TimelinePost( + id: 'Post0', + creatorId: 'test_user', + title: 'Post 0', + category: 'text', + content: "Post 0 content", + likes: 0, + reaction: 0, + createdAt: DateTime.now(), + reactionEnabled: false, + ) + ]; + } +} diff --git a/packages/flutter_timeline/example/pubspec.yaml b/packages/flutter_timeline/example/pubspec.yaml new file mode 100644 index 0000000..9a87739 --- /dev/null +++ b/packages/flutter_timeline/example/pubspec.yaml @@ -0,0 +1,95 @@ +name: example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + flutter_timeline: + path: ../ + flutter_timeline_firebase: + path: ../../flutter_timeline_firebase + intl: ^0.19.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_timeline/example/test/widget_test.dart b/packages/flutter_timeline/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/packages/flutter_timeline/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// 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); + }); +} diff --git a/packages/flutter_timeline_view/example/lib/main.dart b/packages/flutter_timeline_view/example/lib/main.dart deleted file mode 100644 index 568731b..0000000 --- a/packages/flutter_timeline_view/example/lib/main.dart +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import 'package:flutter/material.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Timeline Example', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const MyHomePage(), - ); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - ), - body: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - ], - ), - ), - ); - } -} diff --git a/packages/flutter_timeline_view/example/pubspec.yaml b/packages/flutter_timeline_view/example/pubspec.yaml deleted file mode 100644 index b8097d1..0000000 --- a/packages/flutter_timeline_view/example/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: example -description: Flutter timeline example -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -version: 1.0.0+1 - -environment: - sdk: '>=3.1.3 <4.0.0' - -dependencies: - flutter: - sdk: flutter - cupertino_icons: ^1.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - uses-material-design: true diff --git a/packages/flutter_timeline_view/example/test/widget_test.dart b/packages/flutter_timeline_view/example/test/widget_test.dart deleted file mode 100644 index 73b773e..0000000 --- a/packages/flutter_timeline_view/example/test/widget_test.dart +++ /dev/null @@ -1,14 +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_test/flutter_test.dart'; - -void main() { - test('blank test', () { - expect(true, isTrue); - }); -} From e9b2daed6847ddf6ba3d74ebc124ff253ebb85e2 Mon Sep 17 00:00:00 2001 From: Jacques Date: Tue, 16 Jan 2024 16:21:11 +0100 Subject: [PATCH 02/11] fix: Applied the feedback points and full example --- README.md | 79 ++++- .../flutter_timeline/example/assets/logo.png | Bin 0 -> 20506 bytes .../flutter_timeline/example/lib/main.dart | 87 +++-- .../example/lib/post_screen.dart | 1 - .../example/lib/timeline_service.dart | 12 +- .../flutter_timeline/example/pubspec.yaml | 7 +- .../lib/src/flutter_timeline_userstory.dart | 1 - packages/flutter_timeline/pubspec.yaml | 18 +- .../lib/flutter_timeline_firebase.dart | 1 - .../service/firebase_timeline_service.dart | 37 +- .../src/service/firebase_user_service.dart | 55 --- .../flutter_timeline_firebase/pubspec.yaml | 9 +- .../lib/src/config/timeline_options.dart | 12 +- .../timeline_post_creation_screen.dart | 327 +++++++++--------- .../lib/src/screens/timeline_post_screen.dart | 54 +-- .../lib/src/screens/timeline_screen.dart | 63 ++-- .../lib/src/widgets/timeline_post_widget.dart | 29 +- packages/flutter_timeline_view/pubspec.yaml | 9 +- 18 files changed, 468 insertions(+), 333 deletions(-) create mode 100644 packages/flutter_timeline/example/assets/logo.png delete mode 100644 packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart diff --git a/README.md b/README.md index 67dea13..b63e679 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Flutter Timeline +Flutter Timeline is a package which shows a list posts by a user. This package also has additional features like liking a post and leaving comments. Default this package adds support for a Firebase back-end. You can add your custom back-end (like a Websocket-API) by extending the `CommunityChatInterface` interface from the `flutter_community_chat_interface` package. ![Flutter Timeline GIF](example.gif) @@ -23,7 +24,83 @@ If you are going to use Firebase as the back-end of the Timeline, you should als ``` ## How to use -To use the module within your Flutter-application you should add the following code to the build-method of a chosen widget. +To use the module within your Flutter-application with predefined `Go_router` routes you should add the following: + +To add the `TimelineScreen` add the following code: + +```` +TimelineScreen( + userId: currentUserId, + service: timelineService, + options: timelineOptions, +), +```` + +`TimelineScreen` is supplied with a standard `TimelinePostScreen` which opens the detail page of the selected post. Needed parameter like `TimelineService` and `TimelineOptions` will be the same as the ones supplied to the `TimelineScreen`. + +The standard `TimelinePostScreen` can be overridden by supplying `onPostTap` as shown below. + +``` +TimelineScreen( + userId: currentUserId, + service: timelineService, + options: timelineOptions, + onPostTap: (tappedPost) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: TimelinePostScreen( + userId: currentUserId, + service: timelineService, + options: timelineOptions, + post: post, + onPostDelete: () { + service.deletePost(post); + }, + ), + ), + ), + ); + }, +), +``` + +A standard post creation is provided named: `TimelinePostCreationScreen`. Routing to this has to be done manually. + +``` +TimelinePostCreationScreen( + postCategory: selectedCategory, + userId: currentUserId, + service: timelineService, + options: timelineOptions, + onPostCreated: (post) { + Navigator.of(context).pop(); + }, +), +``` + +The `TimelineOptions` has its own parameters, as specified below: + +| Parameter | Explanation | +|-----------|-------------| +| translations | Ability to provide desired text and tanslations. | +| timelinePostHeight | Sets the height for each post widget in the list of post. If null, the size depends on the size of the image. | +| allowAllDeletion | Determines of users are allowed to delete thier own posts. | +| sortCommentsAscending | Determines if the comments are sorted from old to new or new to old. | +| sortPostsAscending | Determines if the posts are sorted from old to new or new to old. | +| dateFormat | Sets the used date format | +| timeFormat | Sets the used time format | +| buttonBuilder | The ability to provide a custom button for the post creation screen. | +| textInputBuilder | The ability to provide a custom text input widget for the post creation screen. | +| userAvatarBuilder | The ability to provide a custom avatar. | +| anonymousAvatarBuilder | The ability to provide a custom avatar for anonymous users. | +| nameBuilder | The ability to override the standard way of display the post creator name. | +| padding | Padding used for the whole page. | +| iconSize | Size of icons like the comment and like icons. Dafualts to 26. | + + +The `ImagePickerTheme` ans `imagePickerConfig` also have their own parameters, how to use these parameters can be found in [the documentation of the flutter_image_picker package](https://github.com/Iconica-Development/flutter_image_picker). ## Issues diff --git a/packages/flutter_timeline/example/assets/logo.png b/packages/flutter_timeline/example/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9a434e42e6a1785ffbedb83ea7dc39037433c382 GIT binary patch literal 20506 zcmeIaXE>Yf8wVWK(#g}JR!Lh`qeWFwVsr>fZLvpNk5Ma#O%kQ8HdfWDt%(_{wpi&R zYNfG9NQ)4=Vus*N|L^$xetEyV$CKm8;c(~9eVx~No%{Tqd24K_bMCazX#fCl?vbvR zDFAR>>hJp`2kXl6wxuiU&nX{WYZw4<^M11LPT7F|yL8J*_aGUJrIa9{1k|rrpVmTI+clwSt)tS})|`KXTgv0E$)ap9at(XXNbZ z$-Gp3v~pqc^FOiw_w`T7rY8B*aVDF8!Wwjm8mrZ%T;~80LOl6^2rkfJb*aNUc0OX9 z!wumzwK-wS$EN`i85jP3hK)Q1mNr~kxQRLs&Mz_u9sGQTMMWVi$jXblt{(a)Kt=77 z{-CyR*9}=~`Ulo(SN>A*ZUk|ZVeKqf`9|o``5Fn{g~9*llaF6EU!&g;z`gmBhrfsT z9qL#S+F6_59nX$xLr%%8HEm`<^K})_Jf@GkDP=Is8n5Q)@0Q9w z+eV+Kf&hT}HC;CN>_oS>dW%Ui)g}D_}M6hLs`&BaE`|aC-E0i~mBu z2*FFwv3KWZJ}qytLr{>77V{z^{M~%EZ|C6}OPGY-B^}%QUYKdwDxDCx%AdW%AKkJ^ zl{-7y@fhbyJ79>8OR1I#96BP8Gfc~5%(8d^0L3dTntw&|-3WBont4e}t)`{X-`9*Ff1fk&cH3l+VYLSY(V2@CksG$(>% z^{m`R!(4~vnq?chVf+5xoF_g(=iT zwYMg51%wkxnuuq+^_ulqA<$Y!3h`z}djlrO=3)C!v)sCFSnzmd)Jqz8mlFxcrb1@MWu1QIZ;yvRuu~CFu0ih12-h)g*$A88* zP?Zj!ZihVf=1`1~XPv%cEp3la0V7Ef-7>x-%)Zv_#_;3b-i+gsZKc1vBphro2#z-! z{CMrTHE&UNKqmtK#*5fuvTCWm!wdjm-baOk+G%TYI~Q;Lm5bL(+!u$EXUC79FwKW6 zegC}9cJQn1WlEgFQVQqQEv6Nnjp_HT@dq%@Ye3d(8Ke+F!mg%$QQAH+>K@5!tPtbf z=)9vw+d~&;Pw#FZtE#i(0h`~~({8G$>I~=bDk@yKvoPj~8q_NaXXZ_=#(SB8?q7cN z?dc|2?%v2L(|d6@AD2{xd|Iz!KK|q5p&9mc|5@H$@w^U0D}U-XNz&a`wIN#k5x)6b zP}||#|I=I+Cq7Io550fB{kMr3&ULGQ!`sI9-aUhqsP?Lm8l6FNxzxP;WToUpLx;ag zK2Z_pwcIQfL2{p;5;`3r&$8Z%zK3$FA5w+d!_ad=;F;JOqG4}DhRXAGzA*kcS~OV- zteoU_Q7Rig+`?WF>ow7@wB0Jr+V_Q(cqL=s06;9u7!|)Aw6l~c_#mQ~h6>)>HnKjT zM`Q@1e2=o4XF*^fZQ36dK%I+&zV)JjabBYBh#Rnd3joHH7p%BJ+rW?yX4r0Vu-MPh zwpYUk0+V*5X*}TkB)PV2OEY%&=|-^rc3kq|?L8e!7c*Xymi;mYXXP6&N51^so#?Tm zlnH&mB9DiFmEvRB4H`WfnmsE?|JU>CGY9WticNlyqAi!}pHBb$!KV1hXQAJ>eRp~W zr!!?qd9I>ITDaW41t#ye1t0Hx5iL#WcqC6wZ(3pt#K|_e&K~rg`76oR*~-zg+cTWI zM99y|jQhU-PF3;&obFj)p;dvqEk*7mPxpr_b7=v8w3dRDJ$1u0f+__YIPn3i)hR|@ zz%r52NK@YR1=qfu^*EZsyo_Ur-lDPf&oHIB>vqgi5Pv@6jOpxzTkc9x{ueZ@)7QvT zi5U7jrzRmr=RoKR0(0{=B6>BGz574Zn}Ly8u>cA6G>(uxIFVf+Q77*$=HS9o7A8 zzr(H+&JN?MyGO7G{+*B@a%)KYo zX~jPdU$ur6$(=zgWhHkMX|EsBBAJJ|etlb|KoR(_k|UpwtN`=^bnZe@ zteKn*!V;LG!T}I1VX2q)91-4A9umGaSnPG7ce5#^#-*Gau@vdtpssjS75`SWQ*4&l z{Hrhp$)}jy!QE2%<{5$Zf&f)kp~^Lki<2%%3Q;u%3Io;e`R4K_Df2C9{Pn=Dangf5 zWBO_F6NoOZG(NEJlfmv2XI$1=F7KARXCIwVu5406CX4d6nttEfISicvY94*Y1%VB+ z8p5eCSj03Q6P-#9AxaUSSVWP-q}f zm1G*;R#CCxH}-Qwr2W0^**&bzABTxvwXb9CiTzizWkyg&!N0Vsw5$N~N5~M)!rH4vq zFxOFTHJMt@no4q;ASb; z(J~oZps75|xt=4(CiKXm3F!@yO)r|;P8QJUsA`jyC01^|5AV}_EBd2SQyl7$JnY#ZaI|l$juR?KHp>P zoN*yC6Gl0x#GZ#6bFl(k988zmHX%i3CE2it`FCBI;N5)q7n<3^kC*Mtk(W}C5H{Ar9DL$M&>hQU@9JIzTY`)rXf{{h=j3qAl?3YLWcA!;0%{sIL4w z#+zzyYH5X;q9Zqjps0)wD!$C!6^=}G;C+fO`SZ^{9>R#*+_M?4`6)krBl;_UQ$(kx zDzvY6<&p1EpI+U|X>pwW`<=0IDGEkC)y13Gb;Lw?&r>Zy!W@$n#?5{ZnqX?36FOLCt)6XTqfBXfzLmvXw=Sj1fkh`( z*9!SKwYa^cJMbFkDl02rX^Wm74_i#8)u}gRnu_QRc2=-cKP%ORd*t(2pD+VrIab`Fg(TjSv3uR*9u(|{g)mg7+pZsk(8B}5wgFk*V= zz`#3ZBkWHC$y%;&C4{?%7^b5-A(G-ab2Qsal_!@VA8+<`KU-!#^sf7`rHsx<@dG0& z9YubMv7Ccbxps()oc{Yc@1gh2h{oNTF9&Cpy11#zMVS~hrr+((;mUH2nb8G>oaS%_ zj}UmUVrKut+Zhg^I^t95z*pqR=BXLm1R+hU=CyI=pOEPge4}&1XrOs^9m(G!HURk*C{_3CB2DJ8BUcqQ_Wj2kBHHF(@Xa+YQ@iPKdlO_JiM1a0_C; zb7Vu+6R>u;Q>g7%a`RGq#Sr4?g2?*>r)S>t^GJkL!z6mAgFc1*#$dKBhAlm0sB1c1 zTbJ=qxqM>KN-^r=P0&ukk&mf$md_?h2Rd+SdpzKaL2106iJFAFB$bq~a^$vU6~7v5 zqtF5AHgHd$r?EVfGHUSPeSVx|7rZ{#j<_~1wr zzEfyBN2q@qt+Xw&u`d>c#&SIq0nS3-$SYkqyPI2V`KnR+&7a=2kPUghC zc9&IrN+7+_li%rPu6~_oN!Jp6usE%%r2)aW6&QX9inZqA5;(_-c5(B4AF5jhdFP{C{Ok5y)eqKUx>$*!GRX+Cs)akq8e&5Sj2xUJ=IW4vQSiA`y1(4ZaSt8`%IZ5%6R zX)QEBx%5^$YrHehm&ieGEILKXyw^-C?c#uZ?NhB7(@=Mebh1bd&j0$2fELwP6Md|> zctlN9yO8iNRIiS^IH5~;`ZrJD_Jfqi%uN9hMd*2elE6=JB=1iNA$DK)sj`^9X51y+ zqf2BXm|baxb^p34)alAjY>=ecQV&=}7rosqYl9f!Dz8RrFS{6||1-hOdc!g@~v(`y@VxYCqdQ$3#oeb`a8ZR9PeemnbC3)B!QO>G< zakAu1JjXv>w_AS~kLkr9RV){R^&Z4%UP{$L?2+?m4H-zA~o#EG` zw}*)K$(1GXvnRXpK><@OvbkY({$+cfWBvI3ZzaQ5Rt3K~YcE+@@UM#L-Y>s|5B{Ow zEeRS}5OjA486L>Jo9c10+1s>ct1T=#gV>wkl`Vx#pT>&K_w7E|Ob6qc)MSGt<=kYT zj%vUBW0M{^cN^UFl{?-OEesKYy`QsAmrOiVDbu-h3yWBcu@qHAkU#e&5s-7KbxQy9a|7o5n^g`r=vP`AL=D-M)q0T`>0-Mh-x$CrP%Io$+ax^ygCE2c)!V{mSX)r zh=fo;1txc<)S<(D$9OS!Ou^6ek4415!kkv$*zv-z=6XQbT=eP3%rPSb&xKCdA+B^| z&Y7`QiqeBHegu|!w%k2h=A^ITuI~-%`aCrmqT|fjGTp~qL40?xW4sW^>lzF`i+80o zm7Z-;$84JvRLf_ILcU~4`}!$P&^bvjx^8|_oXyX>Xy{NqD-Sja4E5UIJ=G3hIv9N& zzEn5_^Fy`_6jbe1?0u=O6Yg)aAng6#VK6f4*fYDTbG2zoN?r8)SAX+7)Ufdgwlww? zlau+Q_u-P}`P!LAH;SuYn1$vENp17_@m1Gf)nqPUOvySU$XwrwE9Q5+d2{{jnz=p$ zUYx4QgnO@W@Hh&<|Gg%-61k0#_9m`?8S0@m0B{(6A0nd|_WWM&NmJu9nIn9ay}*=a zt!QfWI$r${Eq?HBH^xNB(_-MwfD`)LYS;s#5;@h`CEN1Nn7V8rZ{<{M=BhnK`96xN z%p6si>)G}7A3fi^`X2PL-4)}QAH1X4liggfR15T_6F&E}gxJ*1PvAsyLB~DN2`ueQ zb*u+D%@&g9=3%We9lD41kg0h5o8S4bR*F+Wo(f{$8SvfEvuVv<1lWGr_z#$$Fs0)# z6SkkpoboR}Hm*cFqAIz(z4!FyNz)j?yA_^_^q7q)jTx?#A>n*RoQKRYI5(M`hDeTS8u9jCCu7uV$rQWL;^K8|0l-$ue zu>aGe0y=X28SgG-1X{|%a&4c1Nnn8$(UcuBS#+f15E8wPW7ws&3kaVwZ@K%rXe+@Y zC3qcN<;5rxFsxOzfoLR%VUSGw+IbPi_-^2*cKW{MiIUzsg%-!9Xt<7-)9PSUg^VZB z>36iqEyTX&xxMi*DMeW0FLn|bVu&)|5NH<>R&K9U@?0FJCtK*X2kexUYukAobR;{6 zuI2V-)ZK2H{gbYc##_StchDO3v06Y?-fe`bw1d=NJx=C3D4AUA+KF?1-a$!21}X9^ zek0|~e^-MXHcs4rH<^ft2a-zxg%Ager%yNr|y zQ06&=7jOy}<%a)wA1`-44;Eyh&9-&|S+T6bqip|apM;Z%kFz`=zj{P-uFZ+BCi ziQ_^>+)R_>Yv_|t!LDupBF-zOh*__x`=!}GUiYH3u!>%?4Q*K^@9~+Xg3-|p>taKu zsR=sskQCYR^c27Eoj6Z@Y{9veYiydA_r853IUFXL?R%Yq#6iT3p=XOv-9H^V2hmS6 zEQvhPD!K9@I@}d1ds^%beDUj&rKW1if;j06_^i3uPm_l?hP~c^l)T~{cH74Th90BF zdkc%P)pxq+=ZM+zm?cssL&1eS_3Gtk+iQ3^{J2`%yPGuzVk$!8f(r-kt6D?!jc2^r z%RZnlOlaV8dBByDanwbDU#2C!iNE zg01V(8Fi4{Jd4B%+{P5h@J#*p3fA7$|H#E34jD#QWla98>&G&3DpE}j^5cMG7t?|c zYvbS&h0+OT2P>qGHvZ$EdN1VXNnH05wJev)=4(xzh9}2f0Q1N_L+mO7t2dKA>cXW#bHq`vG<6%`&FSGK6gN=Bhe#dUN(W@ttkrk?e>qE%*VY1O$llk;GLJrIt#Qm@T z*msK!AgrMwTd{w#zf4+Ji@Ua~x{LjMz{XNsrtpmsuVv?V(}buRm%++qC*0}=Lul2W zQ1>PQs1X(lGMi;QTzQ-hEzb&NqrHSR;pwNkAW#q;Q_9Fbv`9~@}SjCN;KFqt`Z?HGx(g}!mx zBMFtU9bXxWH}@N#G)%5)u=3v`Ez+(zY2|(-uX00wp{T?S#P{bXZrRL?kM5iY=IKd6 z)+|#xgZ2evYd{yrf1EldBCdNsXsdRX_E5hh{J=NuCSy&3dkp#9_{NM9|JvKDL-lsc zx{CeDI)P2YSAbn|doguey+0ZPiy>qmVv}F)bF(I)d|hz(L8rawPH%CE2okCGPK+YFBIzTLckC3bc>cv#o=pVph-xa@zivmK5E;u1m@zrH#% z$*)MycD|9DjL$iec3*D$rDftD@Pn}BG2C8gVZHRSvU|i^ayX#w+Vk>0yp41oaxDVMTFWg7;Z?s9fp`h{7aH2!lkPl016>2P!V1d@Ty zyLhqS&O4!Y5Qdwq49C;@Hdhrhqx<7L!W{iJf18UxLbkbhjYRS~TelsIH81zaNV9vX zxDS_gZ@BhKpg~>Qg3vzS+ECeA_FvqypTGBzvErc+)A1Qt=EXlJS)K76<)1fgBu>o_>b{D5Ag8mcf}Z;&5vXJRtQjA zw{0`*Lv)a{@qB>IS4^C`CbW;Z59IEW; z*SzVw?%4vLDQtud%(a~~W_B1gnY_rCGz{DB$+pnS{h%4NTF!V>`)wqssdP+Ww+N6Y zHw$RLu`#!VTM_wl^1APJwRbLAh*2~+_h^dAs4M-)Sb`wiMt^yAGR-=)=iu2vHu8?s z^zI~@m@m23Co&fwODp1S?{6Lv$v(24xQ6DQT{UPk|?V!%vKOw za<4~R*$Y95CDg{tpq`of-;?gwaX8$a^uRMuMEpKZ2-&D$ArpVS;04dzIL6Dp(%4-Q z?qH&%As`|&a5QGWQ%EonNXq~loJr7l6TXt=_g!{C^MZ&UQ`~a$z2hvLq+@zWLDdO& zP&KK$wAjgxlqg8Y22=}(^Z1&9Y}{B4e(bN8n1lR^zF`D{AHE1W0jNu1=dI*oJH0E@ zt^SLfH)z%{MXI0s_tuC5NRd-=3Mf+h@n+1@NZE?s)vl`dXe;%nllOEsu6j=0`BT-h z&`etAN^|v65^mp5P@k_}K^wKu_tS`a&e3UM`P1vq!#B>c-SeUXzlhf5u~qH4uFvdrfx+`r>U;+n1dQ*FD`vZh&K3q?VzU4Bst-0FX_m``91n^9;z zK~i~iCB4?E%@?dg2gMdR$>FYY?sko61w9s(1URX@q~D zL5ip~zA(UjGwXRHi-XV1O>dve?A9?5D1#XXOd8h&kiWhHkGQ~-!85zs`9SIv5)PtD z)nTuRkkmns!#AJs{i}A}y^JKFc0)DUXPb5Qp`=ndJNgT5P52Rz6Hxb#QyBZ}`;BMa z!;ZH#55%wVYok2RdK zjWepL>qR6HsOT|#jsyl~?0okSChcksTIzkH$WLaUGV9P!I5>nLFDV90gGbaf9S4c1 zRcVflb^^bw9SsD`({RY0NIhPL&z;!to2ZgRY4&*?TGcZ5D^3=l;=bt*K@Ed2Ij>Qp z;Sc`26YBkflnwplO|M`n0N>K^NcFhh>RXC}te2{I=o8pD^9yqij|!CrW!>C(!N<5Q z;?^ROU_9iV{&C`PhlUqVeYKx?24*Hox zp;q3M&hpClF%Lnka0RO{uf8xDQghS&dXTHe`j-%+)kWQ4yb5AHMXr;AJ%iS%u&rsU zs!%j>#)pT5xXcGi-9{HB-3?FaoS*# zH7_0$JJ;;sx881sx$Xd4uLBiXkm_F6aOc0d^Rz3Fx)fPX>U-b*oGo_vHLJMR^Wx7s ziIOuscL?otEam4=d%((U0w$nsOPU)q^y%WaYIly>xS2nIlnZLbE{_obQ_Fnn^`0wx zbFVF|(-z{KrDKxfCorP|>YM9PT;o|7w#pTU5@K}fYnaJqcnNs*-CC5M;c(pOlo$`&k z6Plx?5L75jS#PO?Uk)~|v}y6*F;5zk8?s5bv1Y{+@GgN`dS=e>$YJoaV6>WiLPb6d z!+|m{&%x@n?%Xa=28R9ahqIzL+i+^jprpI0Z~mwhNB=^!j7@&iNGh&84!#p}INZ0n z??B#|iS*Jo;f1h5H)%d2ltzjvRs%V~@ZNidK)R==olyn9i_E#}F~w#%r;|a{N>CJ= zs`vT*)kfw@ai)ukxj0B9He+|Iu(#KC1iHME7J&Ll7F>H&-NTuJ8tL#cEq4&YUI1zt z%sLFzcH9f`+qBYaF9|qBHaA7Nu7Db6YNBL(VZ!^XsUR`?8;TQbJFKW0gWeB3S+BT4 zb~K7VgurDVKX*cQs|9~QpP%Q7lGOYF{576ErR>5gN*w)aIq;C*v$G^PhW7FZ#|0w> zdyH1w8piZWT)z|~+pO8$a-Giw$7T7_4k6{60~j6c)R4hjGogE#GjcZleYNBA0pw|* z$dDxJ8^(cyL&$f0$s@T2lTZtzSQ4#tw2$0fpX$rK?$dBR*vw(?H|Lzkw%+HLNE*V&m&by% z&p{Xtrio1styj(!ohhm0!EYnXBVi^8h<8tKK(>Rf z*$S*k(+j`Gmv|`AbPc?u?yA23D>V5{amifcm$XUC6M1tM6%;FSn=gz%ig%%rp-B96 z@FiqpxWP2*JzAf|LacVG$A_3{Znb*1iWCz!a7k4fug&MF))ogW(Wpr~UT4V*PT?%b#t-F_FKQo^9OGTNc|TP9 zvlt7@$8u`zluV%duue2}YMB+WG=w@p^7~kpFCoa#nUP9kch+ksu36i9fkcZtxs6x} zQ&>kkiBsdw+S?fVIzqAOGBxP>o|efShmA#jgepjN`Fp$Nn&qA8H8hZyqzhJ+p*((= z2_Agnw84pv!oI9={$8~B!Dsu+?B({Sm95K*n&-5|gKnkH_ZEadIfR*ZXqOqzE;+SYf~OKld)8hpb0aYtP_uhhu&iuUQDYk@@`@xllRDU zFx-0XO<=Ch4soTKaf#LHTicHC%3=xD`rnu_L)qwz=pdszZB3|TJw@&D;Rjm8Sdv6a zL)oNN%5E%zPCGSYy6PO@`SPRNotzf8g?_1xoynluw-uqXI~RmFDb93st@%idiXV~jO!Qf>|-x4o@0!rwvS>9DL|2LU!qA^ z4Ib=i6N%Z4()wJHN2t5*ti974hnADv8J!xWHtF$0;(bk39UbpVqG<{;E7E$NxFg-wc&shl^6v@emGjg_`iyk`+b~FuL5{$m| zM~+0Gf7=_O&k=QS`^_5^m(q>vvm<50Uy;fU@f_?E<^hFmX%jaH?z}3wu6(tJda;KG z@XHWzIX3@!z+>(ald@IPBrGiS?7Z-(=}|f*3LjUVOhi6CoA5KCV~6fh4`P23GdrVt z`Sp!YvBO-9(N&aE-M%VAu_GTU|Nd-EmxO6iiw-1jri`p%)q5LNe&@D+@oD2#5%`m` z!}?o7E~fW@R<)An&bBW$X!Dy>?u8j;+A$Z?gfXkvx$iiQlq5B60?c{@G>zNTk7j_F zIVJujN(xY1RWz@7SxCM{E`VC$iCInbfshgm+B5>+Pcyh*Gf55+y3+>ffK&Tj?a zl1x%Eyc(`Omw>#e!rJ{VV2tKuC#=&)^;o@y6Uo^3xsZw!!768L*Yyp|ffUnr(UO!~ zAjNPT@+)@Vezv2hquH{6YgK3nVdU&Cxobl_Ur7gPAF4yHjF*t{NE}QWId498;xl@{Y%bcA(py!5(xBxJzx`?4xN_BwOB=a_b|MtyET`39f zN-?iU&ikI41teXQiL)6Xqf5%Q1G`^V4U6QSJ-Nl<(b^I6$)u%1wU36jPLfcn2ac4% z>WB8{l(a_p;>eH6d;vwwalvTOV8kbgp9H4MV11}KR~-hr+M%mKG@9=nK_6oM=3I}% z#sWX#>tkxchh(pEmgN1Y@x8QGw!Wt zq=Bak)A{G+Y}d1jKf<66jF#{Sxi_+kvVX|M_O%utO!1u0pb6K#!jGei zEoYe)0Tj6GqHEcPGm}DYjxM-8&(9V>QG|v|CO{-_}S%f{;oW z3lQ2XIW@AvN`9&ac75BOBw|Y`^w;i6#-wj+P0aC<*9z}VO~N;OG*v#;QyKO5Kw~Qe zrfNA|X|}f;76u*kNGEL?P}t(dA7``91Kl{03mb`ZXmoLSo1gllp@_VO7FV3 zVn-BVlyxHqq;DD zdcMRr#50t*a(!pBh9zEWs<*uRh0)r9WN$*!#)}nE^;%->56H8w(6}cTTQzggzK-p$ z_H?Vr{dI4xJEBma$AqwVTpATTP!vDD6F*vSPEFStcp1~)?TJ=*I{#b1jDY~ zw_eO7rfDCfHr^Zgi$?t)CKQ}FWi~h~IvF-T`$BsY(X|&C#r}N$?abo){U`fxzKrOc zUWEUemEG%{mjX7hh`hs9)crys;u4$5XF|C_X|=h0i_>EY)^2 z>e{bYfFt!2Tn}^~^kRjKAsS8D>7q^_Pop4wvfixy;*VlNHU`Ou3QhYWRaaggO&N(i zqK!QtqWt;TR~d>wQUP{X(<*|#f8%_M%^P!zI?FvLBX2jn_0e4a_MuGEWjs+IR@yZt z`hH@Jbdb(B6n6NVmY2BHB)xAd{$j=X6Hh1_dK&jl>_>=lrACSF0^CGQvo*{sWTtf~ zbbJ96T(qL3>Hc;1OL4;1!;|$lUGkl0Mi0x~<_1bmKzg$R11HV`^EeG3A5;z1w`B~}7>+LhN z3oL7r4wb_N_`&w+ubKH9jm3$*3|sns!(_Trt&RFAb8~T>+}fGioWMV#-`kUf%U%02 z6+Pho)+r6Mk+$BgcaEIO6n3F0w}GS14V3KF(OR6uh@MdTwqwKPcHLk3{h2d*3#G1? z{60H&D9Jh^Al|9cY?E06qP~^d!T9&UPaY}4*MRe}En{L0*eD2mYAt!<<)|OzpE8?< zBOt@BIsNlGf*t~S^r|$XCzs%3T(~&uxex-;znj|OGZgRsh!eNI05u9C)@A`F%)zt313S1{1Aa@ntaw8)-J=I>|+{9_s%LoDFQls`w|d@Mk?N z52Z5NV-h~798fUbtPB7vG`6(2 z6yH&)>@AD*Axwg;3s;dua|RHF-oZ(V-a{z7Hsy9Po8_E1H^YH~PwgZhKu^=q;VY1l z{R*7A(rZvXx20go;z@|Yb&L7H4c*lffWMWXJnjD>vNBXVhDnnO zLHB?<_QsvSq;6I?hJ|uv^D0w97@PJ98ckthb@O&dODh}%DDx>|MIBR!2pwr%=%`z2 z-cwI{tsj3;(QeT<*P)j#*&TfFHy5*EjcjFoPdDZ{W9EJ*6LR%aD2Jppgy4=BDb!Xr z5Ogb4F;lIp^^fJP>W=56u=0p}_NXOCvv~m6nH2~<%Zqt*2U(Mau3$sy$!9&N>F(x2 z<<==FYYG7X@Ov!#I{nj`HTuRG!JUvX@s#`ow)fK4J`(|AVcuCt)3#aKw?`#vR6Mk0 zrHXWdg7Y0(mK2iE>NRjvLaFdb_+$eze$Qj2?R{3DsPWU!!@VunjoM zwwc+c$!eDQMIvJx#V3+|4t9v*$WF4AjlHCq&?YSwL ziW4Md9b2FMdG4qhN-K-0^t6IxYuBs4@1sPSll z-GBX!ZzyG_hBesh+o@hOMIo2|-c%B@Kb5EqO=x@n8f}tw?H(Jz2lRB5cH~G;OD*hn za4bXmbn|U>Y-M)HA2xnY0ANRqH7!JX{18ff*e8;ie(SYq@hf#7-OLw4PT&PY{8^Z_-6FS7uuyx`jM>B(sY)g z`2M@b_2^nb2LU~2irFQ@6*Qajbu-%%l}Et6bv6s%8DLV1v%to@Wo6Rd6}k4_Af@RV zl6aBk*5@_qKjW5|IaIaULDWW0_(K5T59eQfzwXu2oN*E9HE3aDU9S`vP}`JYkZ8(S zTx`b_nhH`>{YC`mb@*jIsLw>A-&BHG5&7$QmC)g`EfP4hOHocATo%)$=WFlL+hN8I zh~Q++MJoEyrot#mAbIH93~95LqOQ09*l+i!DBzROOufN@*DnUc%{K3ilS6IW9F(-w z;a*>4o5Mj(%NV1u@oaGEZc%Y*b@-AsQ6t!ZMPvkvNSF8z>IWVn*LLZ}xm5Yu!x)kn zJA_d4W&#t~g;Wn8A;`WJwfZzs=Hd5l8e^g$)vjLkbYlsvGi)@vxzJ*LUeQb|d}s3^ zH!b+9H6Q}P>N6>tiRa&pMmb?zx`n%LXZy^qHXq#%o&96)YM0hU?{+wSuwvCz3qk0D_W$!}|G|m@Iig4_ zG9jZ=?DITOC8%QrLl9zLPb7b97IR1Qt#{0e-oX?xxZ{JFJ&r}X(1Z;C&W$QE4q+F5 zJM`wH0`&%?$(~n{bQ49)4xk;{>)$;C_*uzfMB(dUF9dApcZ_JaIt3K{3??J}=r0hn zGUj&nrGR(+LprOs4>U0x9Nlq_U777BoX(*eFxMU$qdKc5r-q~fEH3ZIwga>2dgLKVjLd% z?i)*M@F@-m2&rBPl`PmDR|uS>wiG9d&G#_9O|-bvX7hVhF8k1#XaSUvRA$@&z0j4A zd{qY6QgoL!5iG#>H%D<@s~X5qvp|}X(+>0YQG=>me@C5SGE|;5*dwe8HyN}z#fQ&- zDVjVKj=stX?VlvEJLgx5%Dc`Lm?*f4v>c&$osIOqTcFIdHrvE@)~x?`UVqh8*=JxA zI$P0)BRPY=zXu_L7ostRLMBZ8DKekEg;pp5Hm5N+T>NoJyqDgSc67q6Rop^B%)JKf@_ftJ2lsOMUt=&AyL27r=K2ctl=YPpup*?KdqqpLZ>U~LjM(gg0pwL;an=8DPwwp# zn6L`_=EEFrt=?>38e*Ow%<9aEs}%26)o@Mi6tMyB*|NwrI_v!yTJCaJYrb5+e9jil zeF=9oGps!p{?MNw9(4GmV;;?2mJVuF?bAn}Vc~oih&xlwT+SLgAMHM1IJ5E5rsTE~ zNh59mV4S5{@Mnh(3f@K~wB4qo z2eEfWx$-cEd@l5X|Gm{Wl}$C)y`wZB%ebz|ItDBo=WoR9)8X;0$^Ce_4+=Gvg{zvd z?LNz_2G5~+SP}CT>6FT`emAA~(y1ew7`t%vR9gu1b1u~b9?0tKqr^)s?e-7 { var timelineService = TestTimelineService(); + var timelineOptions = TimelineOptions( + textInputBuilder: null, + padding: const EdgeInsets.all(20).copyWith(top: 28), + allowAllDeletion: true, + ); @override Widget build(BuildContext context) { - print('test'); return Scaffold( - floatingActionButton: FloatingActionButton( - onPressed: () { - createPost(); - }, - child: const Icon( - Icons.add, - color: Colors.white, - ), + 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_id', - options: const TimelineOptions(), - onPostTap: (post) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PostScreen( - service: timelineService, - post: post, - ), - ), - ); - }, + userId: 'test_user', service: timelineService, + options: timelineOptions, ), ), ); } - void createPost() { - print('creating post'); + 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( @@ -86,7 +116,10 @@ class _MyHomePageState extends State { likes: 0, reaction: 0, createdAt: DateTime.now(), - reactionEnabled: false, + reactionEnabled: amountOfPosts % 2 == 0 ? false : true, + imageUrl: amountOfPosts % 3 != 0 + ? 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2' + : null, ), ); } diff --git a/packages/flutter_timeline/example/lib/post_screen.dart b/packages/flutter_timeline/example/lib/post_screen.dart index 21d9b77..b72369d 100644 --- a/packages/flutter_timeline/example/lib/post_screen.dart +++ b/packages/flutter_timeline/example/lib/post_screen.dart @@ -22,7 +22,6 @@ class _PostScreenState extends State { body: TimelinePostScreen( userId: 'test_user', service: widget.service, - userService: TestUserService(), options: const TimelineOptions(), post: widget.post, onPostDelete: () { diff --git a/packages/flutter_timeline/example/lib/timeline_service.dart b/packages/flutter_timeline/example/lib/timeline_service.dart index 9e3e539..f4e2099 100644 --- a/packages/flutter_timeline/example/lib/timeline_service.dart +++ b/packages/flutter_timeline/example/lib/timeline_service.dart @@ -15,7 +15,11 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future createPost(TimelinePost post) async { - _posts.add(post); + _posts.add( + post.copyWith( + creator: const TimelinePosterUserModel(userId: 'test_user'), + ), + ); notifyListeners(); return post; } @@ -67,7 +71,6 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future> fetchPosts(String? category) async { - print('fetch posts'); var posts = getMockedPosts(); _posts = posts; notifyListeners(); @@ -111,9 +114,10 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future likePost(String userId, TimelinePost post) async { + print(userId); var updatedPost = post.copyWith( likes: post.likes + 1, - likedBy: post.likedBy?..add(userId), + likedBy: (post.likedBy ?? [])..add(userId), ); _posts = _posts .map( @@ -152,13 +156,11 @@ class TestTimelineService with ChangeNotifier implements TimelineService { id: reactionId, creator: const TimelinePosterUserModel(userId: 'test_user')); - var updatedPost = post.copyWith( reaction: post.reaction + 1, reactions: post.reactions?..add(updatedReaction), ); - _posts = _posts .map( (p) => p.id == post.id ? updatedPost : p, diff --git a/packages/flutter_timeline/example/pubspec.yaml b/packages/flutter_timeline/example/pubspec.yaml index 9a87739..27c1008 100644 --- a/packages/flutter_timeline/example/pubspec.yaml +++ b/packages/flutter_timeline/example/pubspec.yaml @@ -37,8 +37,6 @@ dependencies: cupertino_icons: ^1.0.2 flutter_timeline: path: ../ - flutter_timeline_firebase: - path: ../../flutter_timeline_firebase intl: ^0.19.0 dev_dependencies: @@ -64,9 +62,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index 87d0dcc..b666a73 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -96,7 +96,6 @@ List getTimelineStoryRoutes( userId: configuration.userId, options: configuration.optionsBuilder(context), service: configuration.service, - userService: configuration.userService, post: configuration.service.getPost(state.pathParameters['post']!)!, onPostDelete: () => context.pop(), onUserTap: (user) => configuration.onUserTap?.call(context, user), diff --git a/packages/flutter_timeline/pubspec.yaml b/packages/flutter_timeline/pubspec.yaml index 69e1e87..6deff9a 100644 --- a/packages/flutter_timeline/pubspec.yaml +++ b/packages/flutter_timeline/pubspec.yaml @@ -15,15 +15,17 @@ dependencies: sdk: flutter go_router: any flutter_timeline_view: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_view - ref: 1.0.0 + path: ../flutter_timeline_view + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_view + # ref: 1.0.0 flutter_timeline_interface: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_interface - ref: 1.0.0 + path: ../flutter_timeline_interface + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_interface + # ref: 1.0.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart index 9ad1f86..8a0afdc 100644 --- a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart +++ b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart @@ -7,4 +7,3 @@ library flutter_timeline_firebase; export 'src/config/firebase_timeline_options.dart'; export 'src/service/firebase_timeline_service.dart'; -export 'src/service/firebase_user_service.dart'; diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart index ed95443..d171c34 100644 --- a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart @@ -9,10 +9,13 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.dart'; +import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:uuid/uuid.dart'; -class FirebaseTimelineService with ChangeNotifier implements TimelineService { +class FirebaseTimelineService + with ChangeNotifier + implements TimelineService, TimelineUserService { FirebaseTimelineService({ required TimelineUserService userService, FirebaseApp? app, @@ -30,6 +33,8 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService { late TimelineUserService _userService; late FirebaseTimelineOptions _options; + final Map _users = {}; + List _posts = []; @override @@ -317,4 +322,34 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService { notifyListeners(); return updatedPost; } + + CollectionReference get _userCollection => _db + .collection(_options.usersCollectionName) + .withConverter( + fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( + snapshot.data()!, + snapshot.id, + ), + toFirestore: (user, _) => user.toJson(), + ); + @override + Future getUser(String userId) async { + if (_users.containsKey(userId)) { + return _users[userId]!; + } + var data = (await _userCollection.doc(userId).get()).data(); + + var user = data == null + ? TimelinePosterUserModel(userId: userId) + : TimelinePosterUserModel( + userId: userId, + firstName: data.firstName, + lastName: data.lastName, + imageUrl: data.imageUrl, + ); + + _users[userId] = user; + + return user; + } } diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart deleted file mode 100644 index fb1da17..0000000 --- a/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.dart'; -import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart'; -import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; - -class FirebaseUserService implements TimelineUserService { - FirebaseUserService({ - FirebaseApp? app, - options = const FirebaseTimelineOptions(), - }) { - var appInstance = app ?? Firebase.app(); - _db = FirebaseFirestore.instanceFor(app: appInstance); - _options = options; - } - - late FirebaseFirestore _db; - late FirebaseTimelineOptions _options; - - final Map _users = {}; - - CollectionReference get _userCollection => _db - .collection(_options.usersCollectionName) - .withConverter( - fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( - snapshot.data()!, - snapshot.id, - ), - toFirestore: (user, _) => user.toJson(), - ); - @override - Future getUser(String userId) async { - if (_users.containsKey(userId)) { - return _users[userId]!; - } - var data = (await _userCollection.doc(userId).get()).data(); - - var user = data == null - ? TimelinePosterUserModel(userId: userId) - : TimelinePosterUserModel( - userId: userId, - firstName: data.firstName, - lastName: data.lastName, - imageUrl: data.imageUrl, - ); - - _users[userId] = user; - - return user; - } -} diff --git a/packages/flutter_timeline_firebase/pubspec.yaml b/packages/flutter_timeline_firebase/pubspec.yaml index 39d238d..517b9f5 100644 --- a/packages/flutter_timeline_firebase/pubspec.yaml +++ b/packages/flutter_timeline_firebase/pubspec.yaml @@ -20,10 +20,11 @@ dependencies: uuid: ^4.2.1 flutter_timeline_interface: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_interface - ref: 1.0.0 + path: ../flutter_timeline_interface + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_interface + # ref: 1.0.0 dev_dependencies: flutter_lints: ^2.0.0 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 42e390a..be2a0c4 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -19,20 +19,22 @@ class TimelineOptions { this.allowAllDeletion = false, this.sortCommentsAscending = true, this.sortPostsAscending = false, - this.dateformat, + this.dateFormat, this.timeFormat, this.buttonBuilder, this.textInputBuilder, this.userAvatarBuilder, this.anonymousAvatarBuilder, this.nameBuilder, + this.padding = const EdgeInsets.symmetric(vertical: 12.0), + this.iconSize = 26, }); /// Theming options for the timeline final TimelineTheme theme; /// The format to display the post date in - final DateFormat? dateformat; + final DateFormat? dateFormat; /// The format to display the post time in final DateFormat? timeFormat; @@ -71,6 +73,12 @@ class TimelineOptions { /// ImagePickerConfig can be used to define the /// size and quality for the uploaded image. final ImagePickerConfig imagePickerConfig; + + /// The padding between posts in the timeline + final EdgeInsets padding; + + /// Size of icons like the comment and like icons. Dafualts to 26 + final double iconSize; } typedef ButtonBuilder = Widget Function( 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 ae01025..3e52ab9 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 @@ -17,7 +17,6 @@ class TimelinePostCreationScreen extends StatefulWidget { required this.onPostCreated, required this.service, required this.options, - this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16), super.key, }); @@ -34,9 +33,6 @@ class TimelinePostCreationScreen extends StatefulWidget { /// The options for the timeline final TimelineOptions options; - /// The padding around the screen - final EdgeInsets padding; - @override State createState() => _TimelinePostCreationScreenState(); @@ -92,173 +88,176 @@ class _TimelinePostCreationScreenState var theme = Theme.of(context); return Padding( - padding: widget.padding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.options.translations.title, - style: theme.textTheme.displaySmall, - ), - widget.options.textInputBuilder?.call( - titleController, - null, - '', - ) ?? - TextField( - controller: titleController, - ), - const SizedBox(height: 16), - Text( - widget.options.translations.content, - style: theme.textTheme.displaySmall, - ), - const SizedBox(height: 4), - Text( - widget.options.translations.contentDescription, - style: theme.textTheme.bodyMedium, - ), - // input field for the content - SizedBox( - height: 100, - child: TextField( - controller: contentController, - textCapitalization: TextCapitalization.sentences, - expands: true, - maxLines: null, - minLines: null, + padding: widget.options.padding, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.options.translations.title, + style: theme.textTheme.displaySmall, ), - ), - const SizedBox( - height: 16, - ), - // input field for the content - Text( - widget.options.translations.uploadImage, - style: theme.textTheme.displaySmall, - ), - Text( - widget.options.translations.uploadImageDescription, - style: theme.textTheme.bodyMedium, - ), - // image picker field - const SizedBox( - height: 8, - ), - Stack( - children: [ - GestureDetector( - onTap: () async { - // open a dialog to choose between camera and gallery - var result = await showModalBottomSheet( - context: context, - builder: (context) => Container( - padding: const EdgeInsets.all(8.0), - color: Colors.black, - child: ImagePicker( - imagePickerConfig: widget.options.imagePickerConfig, - imagePickerTheme: widget.options.imagePickerTheme, - ), - ), - ); - if (result != null) { - setState(() { - image = result; - }); - } - checkIfEditingDone(); - }, - child: image != null - ? ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: Image.memory( - image!, - width: double.infinity, - height: 150.0, - fit: BoxFit.cover, - // give it a rounded border + widget.options.textInputBuilder?.call( + titleController, + null, + '', + ) ?? + TextField( + controller: titleController, + ), + const SizedBox(height: 16), + Text( + widget.options.translations.content, + style: theme.textTheme.displaySmall, + ), + const SizedBox(height: 4), + Text( + widget.options.translations.contentDescription, + style: theme.textTheme.bodyMedium, + ), + // input field for the content + SizedBox( + height: 100, + child: TextField( + controller: contentController, + textCapitalization: TextCapitalization.sentences, + expands: true, + maxLines: null, + minLines: null, + ), + ), + const SizedBox( + height: 16, + ), + // input field for the content + Text( + widget.options.translations.uploadImage, + style: theme.textTheme.displaySmall, + ), + Text( + widget.options.translations.uploadImageDescription, + style: theme.textTheme.bodyMedium, + ), + // image picker field + const SizedBox( + height: 8, + ), + Stack( + children: [ + GestureDetector( + onTap: () async { + // open a dialog to choose between camera and gallery + var result = await showModalBottomSheet( + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(8.0), + color: theme.colorScheme.background, + child: ImagePicker( + imagePickerConfig: widget.options.imagePickerConfig, + imagePickerTheme: widget.options.imagePickerTheme, ), - ) - : DottedBorder( - radius: const Radius.circular(8.0), - color: theme.textTheme.displayMedium?.color ?? - Colors.white, - child: const SizedBox( - width: double.infinity, - height: 150.0, - child: Icon( - Icons.image, - size: 32, + ), + ); + if (result != null) { + setState(() { + image = result; + }); + } + checkIfEditingDone(); + }, + child: image != null + ? ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.memory( + image!, + width: double.infinity, + height: 150.0, + fit: BoxFit.cover, + // give it a rounded border + ), + ) + : DottedBorder( + radius: const Radius.circular(8.0), + color: theme.textTheme.displayMedium?.color ?? + Colors.white, + child: const SizedBox( + width: double.infinity, + height: 150.0, + child: Icon( + Icons.image, + size: 32, + ), ), ), - ), - ), - // if an image is selected, show a delete button - if (image != null) ...[ - Positioned( - top: 8, - right: 8, - child: GestureDetector( - onTap: () { - setState(() { - image = null; - }); - checkIfEditingDone(); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(8.0), - ), - child: const Icon( - Icons.delete, - color: Colors.white, - ), - ), - ), ), - ], - ], - ), - - const SizedBox(height: 16), - - Text( - widget.options.translations.commentsTitle, - style: theme.textTheme.displaySmall, - ), - Text( - widget.options.translations.allowCommentsDescription, - style: theme.textTheme.bodyMedium, - ), - // radio buttons for yes or no - Switch( - value: allowComments, - onChanged: (newValue) { - setState(() { - allowComments = newValue; - }); - }, - ), - const Spacer(), - Align( - alignment: Alignment.bottomCenter, - child: (widget.options.buttonBuilder != null) - ? widget.options.buttonBuilder!( - context, - onPostCreated, - widget.options.translations.checkPost, - enabled: editingDone, - ) - : ElevatedButton( - onPressed: editingDone ? onPostCreated : null, - child: Text( - widget.options.translations.checkPost, - style: theme.textTheme.bodyMedium, + // if an image is selected, show a delete button + if (image != null) ...[ + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () { + setState(() { + image = null; + }); + checkIfEditingDone(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(8.0), + ), + child: const Icon( + Icons.delete, + color: Colors.white, + ), + ), ), ), - ), - ], + ], + ], + ), + + const SizedBox(height: 16), + + Text( + widget.options.translations.commentsTitle, + style: theme.textTheme.displaySmall, + ), + Text( + widget.options.translations.allowCommentsDescription, + style: theme.textTheme.bodyMedium, + ), + // radio buttons for yes or no + Switch( + value: allowComments, + onChanged: (newValue) { + setState(() { + allowComments = newValue; + }); + }, + ), + // const Spacer(), + Align( + alignment: Alignment.bottomCenter, + child: (widget.options.buttonBuilder != null) + ? widget.options.buttonBuilder!( + context, + onPostCreated, + widget.options.translations.checkPost, + enabled: editingDone, + ) + : ElevatedButton( + onPressed: editingDone ? onPostCreated : null, + child: Text( + widget.options.translations.checkPost, + style: theme.textTheme.bodyMedium, + ), + ), + ), + ], + ), ), ); } diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index 6082096..623f420 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -18,12 +18,10 @@ class TimelinePostScreen extends StatefulWidget { const TimelinePostScreen({ required this.userId, required this.service, - required this.userService, required this.options, required this.post, required this.onPostDelete, this.onUserTap, - this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16), super.key, }); @@ -33,18 +31,12 @@ class TimelinePostScreen extends StatefulWidget { /// The timeline service to fetch the post details final TimelineService service; - /// The user service to fetch the profile picture of the user - final TimelineUserService userService; - /// Options to configure the timeline screens final TimelineOptions options; /// The post to show final TimelinePost post; - /// The padding around the screen - final EdgeInsets padding; - /// If this is not null, the user can tap on the user avatar or name final Function(String userId)? onUserTap; @@ -58,6 +50,16 @@ class _TimelinePostScreenState extends State { TimelinePost? post; bool isLoading = true; + late var textInputBuilder = widget.options.textInputBuilder ?? + (controller, suffixIcon, hintText) => TextField( + textCapitalization: TextCapitalization.sentences, + controller: controller, + decoration: InputDecoration( + hintText: hintText, + suffixIcon: suffixIcon, + ), + ); + @override void initState() { super.initState(); @@ -90,7 +92,7 @@ class _TimelinePostScreenState extends State { @override Widget build(BuildContext context) { var theme = Theme.of(context); - var dateFormat = widget.options.dateformat ?? + var dateFormat = widget.options.dateFormat ?? DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode); var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm'); if (isLoading) { @@ -127,7 +129,7 @@ class _TimelinePostScreenState extends State { }, child: SingleChildScrollView( child: Padding( - padding: widget.padding, + padding: widget.options.padding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -246,11 +248,14 @@ class _TimelinePostScreenState extends State { ), ); }, - child: widget.options.theme.likedIcon ?? - Icon( - Icons.thumb_up_rounded, - color: widget.options.theme.iconColor, - ), + child: Container( + color: Colors.transparent, + child: widget.options.theme.likedIcon ?? + Icon( + Icons.thumb_up_rounded, + color: widget.options.theme.iconColor, + ), + ), ), ] else ...[ InkWell( @@ -262,11 +267,15 @@ class _TimelinePostScreenState extends State { ), ); }, - child: widget.options.theme.likeIcon ?? - Icon( - Icons.thumb_up_alt_outlined, - color: widget.options.theme.iconColor, - ), + child: Container( + color: Colors.transparent, + child: widget.options.theme.likeIcon ?? + Icon( + Icons.thumb_up_alt_outlined, + color: widget.options.theme.iconColor, + size: widget.options.iconSize, + ), + ), ), ], const SizedBox(width: 8), @@ -275,6 +284,7 @@ class _TimelinePostScreenState extends State { Icon( Icons.chat_bubble_outline_rounded, color: widget.options.theme.iconColor, + size: widget.options.iconSize, ), ], ), @@ -481,14 +491,14 @@ class _TimelinePostScreenState extends State { Align( alignment: Alignment.bottomCenter, child: ReactionBottom( - messageInputBuilder: widget.options.textInputBuilder!, + messageInputBuilder: textInputBuilder, onPressSelectImage: () async { // open the image picker var result = await showModalBottomSheet( context: context, builder: (context) => Container( padding: const EdgeInsets.all(8.0), - color: Colors.black, + color: theme.colorScheme.background, child: ImagePicker( imagePickerConfig: widget.options.imagePickerConfig, imagePickerTheme: widget.options.imagePickerTheme, 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 cf0f214..811b7d6 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -6,20 +6,18 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; -import 'package:flutter_timeline_view/src/config/timeline_options.dart'; -import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; class TimelineScreen extends StatefulWidget { const TimelineScreen({ required this.userId, - required this.options, - required this.onPostTap, required this.service, + required this.options, + this.scrollController, + this.onPostTap, this.onUserTap, this.posts, - this.controller, this.timelineCategoryFilter, - this.padding = const EdgeInsets.symmetric(vertical: 12.0), super.key, }); @@ -32,8 +30,8 @@ class TimelineScreen extends StatefulWidget { /// All the configuration options for the timelinescreens and widgets final TimelineOptions options; - /// The controller for the scroll view - final ScrollController? controller; + /// The controller for the scroll view + final ScrollController? scrollController; /// The string to filter the timeline by category final String? timelineCategoryFilter; @@ -43,26 +41,25 @@ class TimelineScreen extends StatefulWidget { final List? 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; - /// The padding between posts in the timeline - final EdgeInsets padding; - @override State createState() => _TimelineScreenState(); } class _TimelineScreenState extends State { late ScrollController controller; + late var service = widget.service; + bool isLoading = true; @override void initState() { super.initState(); - controller = widget.controller ?? ScrollController(); + controller = widget.scrollController ?? ScrollController(); unawaited(loadPosts()); } @@ -74,10 +71,10 @@ class _TimelineScreenState extends State { // Build the list of posts return ListenableBuilder( - listenable: widget.service, + listenable: service, builder: (context, _) { - var posts = widget.posts ?? - widget.service.getPosts(widget.timelineCategoryFilter); + var posts = + widget.posts ?? service.getPosts(widget.timelineCategoryFilter); posts = posts .where( (p) => @@ -98,18 +95,40 @@ class _TimelineScreenState extends State { children: [ ...posts.map( (post) => Padding( - padding: widget.padding, + padding: widget.options.padding, child: TimelinePostWidget( userId: widget.userId, options: widget.options, post: post, height: widget.options.timelinePostHeight, - onTap: () => widget.onPostTap.call(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); + }, + ), + ), + ), + ); + }, onTapLike: () async => - widget.service.likePost(widget.userId, post), + service.likePost(widget.userId, post), onTapUnlike: () async => - widget.service.unlikePost(widget.userId, post), - onPostDelete: () async => widget.service.deletePost(post), + service.unlikePost(widget.userId, post), + onPostDelete: () async => service.deletePost(post), onUserTap: widget.onUserTap, ), ), @@ -136,7 +155,7 @@ class _TimelineScreenState extends State { Future loadPosts() async { if (widget.posts != null) return; try { - await widget.service.fetchPosts(widget.timelineCategoryFilter); + await service.fetchPosts(widget.timelineCategoryFilter); setState(() { isLoading = false; }); diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 88a21e6..be363a0 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -153,20 +153,28 @@ class TimelinePostWidget extends StatelessWidget { if (post.likedBy?.contains(userId) ?? false) ...[ InkWell( onTap: onTapUnlike, - child: options.theme.likedIcon ?? - Icon( - Icons.thumb_up_rounded, - color: options.theme.iconColor, - ), + child: Container( + color: Colors.transparent, + child: options.theme.likedIcon ?? + Icon( + Icons.thumb_up_rounded, + color: options.theme.iconColor, + size: options.iconSize, + ), + ), ), ] else ...[ InkWell( onTap: onTapLike, - child: options.theme.likeIcon ?? - Icon( - Icons.thumb_up_alt_outlined, - color: options.theme.iconColor, - ), + child: Container( + color: Colors.transparent, + child: options.theme.likeIcon ?? + Icon( + Icons.thumb_up_alt_outlined, + color: options.theme.iconColor, + size: options.iconSize, + ), + ), ), ], const SizedBox(width: 8), @@ -175,6 +183,7 @@ class TimelinePostWidget extends StatelessWidget { Icon( Icons.chat_bubble_outline_rounded, color: options.theme.iconColor, + size: options.iconSize, ), ], ), diff --git a/packages/flutter_timeline_view/pubspec.yaml b/packages/flutter_timeline_view/pubspec.yaml index 96ade42..74f004f 100644 --- a/packages/flutter_timeline_view/pubspec.yaml +++ b/packages/flutter_timeline_view/pubspec.yaml @@ -20,10 +20,11 @@ dependencies: flutter_html: ^3.0.0-beta.2 flutter_timeline_interface: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_interface - ref: 1.0.0 + path: ../flutter_timeline_interface + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_interface + # ref: 1.0.0 flutter_image_picker: git: url: https://github.com/Iconica-Development/flutter_image_picker From 60747d30d88a7aed3a55c0d5810b293a0b370774 Mon Sep 17 00:00:00 2001 From: niels Date: Wed, 17 Jan 2024 13:14:55 +0100 Subject: [PATCH 03/11] feat: buddy merge --- packages/flutter_timeline/pubspec.yaml | 24 +- .../lib/src/config/timeline_options.dart | 26 ++ .../lib/src/screens/timeline_post_screen.dart | 40 ++- .../lib/src/screens/timeline_screen.dart | 1 + .../lib/src/widgets/tappable_image.dart | 168 +++++++++++ .../lib/src/widgets/timeline_post_widget.dart | 267 ++++++++++++------ packages/flutter_timeline_view/pubspec.yaml | 10 +- 7 files changed, 431 insertions(+), 105 deletions(-) create mode 100644 packages/flutter_timeline_view/lib/src/widgets/tappable_image.dart diff --git a/packages/flutter_timeline/pubspec.yaml b/packages/flutter_timeline/pubspec.yaml index 69e1e87..3564c97 100644 --- a/packages/flutter_timeline/pubspec.yaml +++ b/packages/flutter_timeline/pubspec.yaml @@ -14,16 +14,22 @@ dependencies: flutter: sdk: flutter go_router: any - flutter_timeline_view: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_view - ref: 1.0.0 + # flutter_timeline_view: + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_view + # ref: 1.0.0 + # flutter_timeline_interface: + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_interface + # ref: 1.0.0 + flutter_timeline_interface: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_interface - ref: 1.0.0 + path: ../flutter_timeline_interface + flutter_timeline_view: + path: ../flutter_timeline_view + dev_dependencies: flutter_lints: ^2.0.0 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 42e390a..2c84c6f 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -19,10 +19,21 @@ class TimelineOptions { this.allowAllDeletion = false, this.sortCommentsAscending = true, this.sortPostsAscending = false, + this.doubleTapTolike = false, + this.iconsWithValues = false, + this.likeAndDislikeIconsForDoubleTap = const ( + Icon( + Icons.favorite_rounded, + color: Color(0xFFC3007A), + ), + null, + ), + this.itemInfoBuilder, this.dateformat, this.timeFormat, this.buttonBuilder, this.textInputBuilder, + this.dividerBuilder, this.userAvatarBuilder, this.anonymousAvatarBuilder, this.nameBuilder, @@ -71,6 +82,21 @@ class TimelineOptions { /// ImagePickerConfig can be used to define the /// size and quality for the uploaded image. final ImagePickerConfig imagePickerConfig; + + /// Whether to allow double tap to like + final bool doubleTapTolike; + + /// The icons to display when double tap to like is enabled + final (Icon?, Icon?) likeAndDislikeIconsForDoubleTap; + + /// Whether to display the icons with values + final bool iconsWithValues; + + /// The builder for the item info, all below the like and comment buttons + final Widget Function({required TimelinePost post})? itemInfoBuilder; + + /// The builder for the divider + final Widget Function()? dividerBuilder; } typedef ButtonBuilder = Widget Function( diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index 6082096..b54f40e 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -12,6 +12,7 @@ import 'package:flutter_image_picker/flutter_image_picker.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; 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 { @@ -223,11 +224,40 @@ class _TimelinePostScreenState extends State { const SizedBox(height: 8), ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: CachedNetworkImage( - width: double.infinity, - imageUrl: post.imageUrl!, - fit: BoxFit.fitHeight, - ), + child: widget.options.doubleTapTolike + ? TappableImage( + likeAndDislikeIcon: widget + .options.likeAndDislikeIconsForDoubleTap, + post: post, + userId: widget.userId, + onLike: ({required bool liked}) async { + var userId = widget.userId; + + late TimelinePost result; + + if (!liked) { + result = await widget.service.likePost( + userId, + post, + ); + } else { + result = await widget.service.unlikePost( + userId, + post, + ); + } + + await loadPostDetails(); + + return result.likedBy?.contains(userId) ?? + false; + }, + ) + : CachedNetworkImage( + width: double.infinity, + imageUrl: post.imageUrl!, + fit: BoxFit.fitHeight, + ), ), ], const SizedBox( 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 cf0f214..dad384c 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -100,6 +100,7 @@ class _TimelineScreenState extends State { (post) => Padding( padding: widget.padding, child: TimelinePostWidget( + service: widget.service, userId: widget.userId, options: widget.options, post: post, diff --git a/packages/flutter_timeline_view/lib/src/widgets/tappable_image.dart b/packages/flutter_timeline_view/lib/src/widgets/tappable_image.dart new file mode 100644 index 0000000..24fe999 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/tappable_image.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; + +class TappableImage extends StatefulWidget { + const TappableImage({ + required this.post, + required this.onLike, + required this.userId, + required this.likeAndDislikeIcon, + super.key, + }); + + final TimelinePost post; + final String userId; + final Future Function({required bool liked}) onLike; + final (Icon?, Icon?) likeAndDislikeIcon; + + @override + State createState() => _TappableImageState(); +} + +class _TappableImageState extends State + with SingleTickerProviderStateMixin { + late AnimationController animationController; + late Animation animation; + bool loading = false; + + @override + void initState() { + super.initState(); + + animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 350), + ); + + animation = CurvedAnimation( + parent: animationController, + curve: Curves.ease, + ); + + animationController.addListener(listener); + } + + void listener() { + setState(() {}); + } + + @override + void dispose() { + animationController.removeListener(listener); + animationController.dispose(); + super.dispose(); + } + + void startAnimation() { + animationController.forward(); + } + + void reverseAnimation() { + animationController.reverse(); + } + + @override + Widget build(BuildContext context) => InkWell( + onDoubleTap: () async { + if (loading) { + return; + } + loading = true; + await animationController.forward(); + + var liked = await widget.onLike( + liked: widget.post.likedBy?.contains( + widget.userId, + ) ?? + false, + ); + + if (context.mounted) { + await showDialog( + barrierDismissible: false, + barrierColor: Colors.transparent, + context: context, + builder: (context) => HeartAnimation( + duration: const Duration(milliseconds: 200), + liked: liked, + likeAndDislikeIcon: widget.likeAndDislikeIcon, + ), + ); + } + await animationController.reverse(); + loading = false; + }, + child: Transform.translate( + offset: Offset(0, animation.value * -32), + child: Transform.scale( + scale: 1 + animation.value * 0.1, + child: CachedNetworkImage( + imageUrl: widget.post.imageUrl ?? '', + width: double.infinity, + fit: BoxFit.fitHeight, + ), + ), + ), + ); +} + +class HeartAnimation extends StatefulWidget { + const HeartAnimation({ + required this.duration, + required this.liked, + required this.likeAndDislikeIcon, + super.key, + }); + + final Duration duration; + final bool liked; + final (Icon?, Icon?) likeAndDislikeIcon; + + @override + State createState() => _HeartAnimationState(); +} + +class _HeartAnimationState extends State { + late bool active; + + @override + void initState() { + super.initState(); + active = widget.liked; + unawaited( + Future.delayed(const Duration(milliseconds: 100)).then((value) async { + active = widget.liked; + var navigator = Navigator.of(context); + await Future.delayed(widget.duration); + navigator.pop(); + }), + ); + } + + @override + Widget build(BuildContext context) => AnimatedOpacity( + opacity: widget.likeAndDislikeIcon.$1 != null && + widget.likeAndDislikeIcon.$2 != null + ? 1 + : active + ? 1 + : 0, + duration: widget.duration, + curve: Curves.decelerate, + child: AnimatedScale( + scale: widget.likeAndDislikeIcon.$1 != null && + widget.likeAndDislikeIcon.$2 != null + ? 10 + : active + ? 10 + : 1, + duration: widget.duration, + child: active + ? widget.likeAndDislikeIcon.$1 + : widget.likeAndDislikeIcon.$2, + ), + ); +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 88a21e6..b81bd28 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -6,8 +6,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; +import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; -class TimelinePostWidget extends StatelessWidget { +class TimelinePostWidget extends StatefulWidget { const TimelinePostWidget({ required this.userId, required this.options, @@ -17,6 +18,7 @@ class TimelinePostWidget extends StatelessWidget { required this.onTapLike, required this.onTapUnlike, required this.onPostDelete, + required this.service, this.onUserTap, super.key, }); @@ -33,44 +35,51 @@ class TimelinePostWidget extends StatelessWidget { final VoidCallback onTapLike; final VoidCallback onTapUnlike; final VoidCallback onPostDelete; + final TimelineService service; /// If this is not null, the user can tap on the user avatar or name final Function(String userId)? onUserTap; + @override + State createState() => _TimelinePostWidgetState(); +} + +class _TimelinePostWidgetState extends State { @override Widget build(BuildContext context) { var theme = Theme.of(context); return InkWell( - onTap: onTap, + onTap: widget.onTap, child: SizedBox( - height: post.imageUrl != null ? height : null, + height: widget.post.imageUrl != null ? widget.height : null, width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - if (post.creator != null) + if (widget.post.creator != null) InkWell( - onTap: onUserTap != null - ? () => onUserTap?.call(post.creator!.userId) + onTap: widget.onUserTap != null + ? () => + widget.onUserTap?.call(widget.post.creator!.userId) : null, child: Row( children: [ - if (post.creator!.imageUrl != null) ...[ - options.userAvatarBuilder?.call( - post.creator!, + if (widget.post.creator!.imageUrl != null) ...[ + widget.options.userAvatarBuilder?.call( + widget.post.creator!, 40, ) ?? CircleAvatar( radius: 20, backgroundImage: CachedNetworkImageProvider( - post.creator!.imageUrl!, + widget.post.creator!.imageUrl!, ), ), ] else ...[ - options.anonymousAvatarBuilder?.call( - post.creator!, + widget.options.anonymousAvatarBuilder?.call( + widget.post.creator!, 40, ) ?? const CircleAvatar( @@ -82,22 +91,24 @@ class TimelinePostWidget extends StatelessWidget { ], const SizedBox(width: 10), Text( - options.nameBuilder?.call(post.creator) ?? - post.creator?.fullName ?? - options.translations.anonymousUser, - style: - options.theme.textStyles.postCreatorTitleStyle ?? - theme.textTheme.titleMedium, + widget.options.nameBuilder + ?.call(widget.post.creator) ?? + widget.post.creator?.fullName ?? + widget.options.translations.anonymousUser, + style: widget.options.theme.textStyles + .postCreatorTitleStyle ?? + theme.textTheme.titleMedium, ), ], ), ), const Spacer(), - if (options.allowAllDeletion || post.creator?.userId == userId) + if (widget.options.allowAllDeletion || + widget.post.creator?.userId == widget.userId) PopupMenuButton( onSelected: (value) { if (value == 'delete') { - onPostDelete(); + widget.onPostDelete(); } }, itemBuilder: (BuildContext context) => @@ -107,40 +118,67 @@ class TimelinePostWidget extends StatelessWidget { child: Row( children: [ Text( - options.translations.deletePost, - style: options.theme.textStyles.deletePostStyle ?? + widget.options.translations.deletePost, + style: widget.options.theme.textStyles + .deletePostStyle ?? theme.textTheme.bodyMedium, ), const SizedBox(width: 8), - options.theme.deleteIcon ?? + widget.options.theme.deleteIcon ?? Icon( Icons.delete, - color: options.theme.iconColor, + color: widget.options.theme.iconColor, ), ], ), ), ], - child: options.theme.moreIcon ?? + child: widget.options.theme.moreIcon ?? Icon( Icons.more_horiz_rounded, - color: options.theme.iconColor, + color: widget.options.theme.iconColor, ), ), ], ), // image of the post - if (post.imageUrl != null) ...[ + if (widget.post.imageUrl != null) ...[ const SizedBox(height: 8), Flexible( - flex: height != null ? 1 : 0, + flex: widget.height != null ? 1 : 0, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: CachedNetworkImage( - width: double.infinity, - imageUrl: post.imageUrl!, - fit: BoxFit.fitWidth, - ), + child: widget.options.doubleTapTolike + ? TappableImage( + likeAndDislikeIcon: + widget.options.likeAndDislikeIconsForDoubleTap, + post: widget.post, + userId: widget.userId, + onLike: ({required bool liked}) async { + var userId = widget.userId; + + late TimelinePost result; + + if (!liked) { + result = await widget.service.likePost( + userId, + widget.post, + ); + } else { + result = await widget.service.unlikePost( + userId, + widget.post, + ); + } + + return result.likedBy?.contains(userId) ?? false; + }, + ) + : CachedNetworkImage( + width: double.infinity, + imageUrl: widget.post.imageUrl!, + fit: BoxFit.fitWidth, + ), ), ), ], @@ -148,69 +186,124 @@ class TimelinePostWidget extends StatelessWidget { height: 8, ), // post information - Row( - children: [ - if (post.likedBy?.contains(userId) ?? false) ...[ - InkWell( - onTap: onTapUnlike, - child: options.theme.likedIcon ?? + if (widget.options.iconsWithValues) + Row( + children: [ + TextButton.icon( + onPressed: () async { + var userId = widget.userId; + + var liked = + widget.post.likedBy?.contains(userId) ?? false; + + if (!liked) { + await widget.service.likePost( + userId, + widget.post, + ); + } else { + await widget.service.unlikePost( + userId, + widget.post, + ); + } + }, + icon: widget.options.theme.likeIcon ?? Icon( - Icons.thumb_up_rounded, - color: options.theme.iconColor, - ), - ), - ] else ...[ - InkWell( - onTap: onTapLike, - child: options.theme.likeIcon ?? - Icon( - Icons.thumb_up_alt_outlined, - color: options.theme.iconColor, + widget.post.likedBy?.contains(widget.userId) ?? false + ? Icons.favorite + : Icons.favorite_outline_outlined, ), + label: Text('${widget.post.likes}'), ), + if (widget.post.reactionEnabled) + TextButton.icon( + onPressed: widget.onTap, + icon: widget.options.theme.commentIcon ?? + const Icon( + Icons.chat_bubble_outline_outlined, + ), + label: Text('${widget.post.reaction}'), + ), ], - const SizedBox(width: 8), - if (post.reactionEnabled) - options.theme.commentIcon ?? - Icon( - Icons.chat_bubble_outline_rounded, - color: options.theme.iconColor, - ), - ], - ), + ) + else + Row( + children: [ + if (widget.post.likedBy?.contains(widget.userId) ?? + false) ...[ + InkWell( + onTap: widget.onTapUnlike, + child: widget.options.theme.likedIcon ?? + Icon( + Icons.thumb_up_rounded, + color: widget.options.theme.iconColor, + ), + ), + ] else ...[ + InkWell( + onTap: widget.onTapLike, + child: widget.options.theme.likeIcon ?? + Icon( + Icons.thumb_up_alt_outlined, + color: widget.options.theme.iconColor, + ), + ), + ], + const SizedBox(width: 8), + if (widget.post.reactionEnabled) + widget.options.theme.commentIcon ?? + Icon( + Icons.chat_bubble_outline_rounded, + color: widget.options.theme.iconColor, + ), + ], + ), + const SizedBox( height: 8, ), - Text( - '${post.likes} ${options.translations.likesTitle}', - style: options.theme.textStyles.listPostLikeTitleAndAmount ?? - theme.textTheme.titleSmall, - ), - const SizedBox(height: 4), - Text.rich( - TextSpan( - text: options.nameBuilder?.call(post.creator) ?? - post.creator?.fullName ?? - options.translations.anonymousUser, - style: options.theme.textStyles.listCreatorNameStyle ?? - theme.textTheme.titleSmall, - children: [ - const TextSpan(text: ' '), - TextSpan( - text: post.title, - style: options.theme.textStyles.listPostTitleStyle ?? - theme.textTheme.bodyMedium, - ), - ], + if (widget.options.itemInfoBuilder != null) ...[ + widget.options.itemInfoBuilder!( + post: widget.post, ), - ), - const SizedBox(height: 4), - Text( - options.translations.viewPost, - style: options.theme.textStyles.viewPostStyle ?? - theme.textTheme.bodySmall, - ), + ] else ...[ + Text( + '${widget.post.likes} ' + '${widget.options.translations.likesTitle}', + style: widget + .options.theme.textStyles.listPostLikeTitleAndAmount ?? + theme.textTheme.titleSmall, + ), + const SizedBox(height: 4), + Text.rich( + TextSpan( + text: widget.options.nameBuilder?.call(widget.post.creator) ?? + widget.post.creator?.fullName ?? + widget.options.translations.anonymousUser, + style: widget.options.theme.textStyles.listCreatorNameStyle ?? + theme.textTheme.titleSmall, + children: [ + const TextSpan(text: ' '), + TextSpan( + text: widget.post.title, + style: + widget.options.theme.textStyles.listPostTitleStyle ?? + theme.textTheme.bodyMedium, + ), + ], + ), + ), + const SizedBox(height: 4), + Text( + widget.options.translations.viewPost, + style: widget.options.theme.textStyles.viewPostStyle ?? + theme.textTheme.bodySmall, + ), + ], + if (widget.options.dividerBuilder != null) + widget.options.dividerBuilder!(), ], ), ), diff --git a/packages/flutter_timeline_view/pubspec.yaml b/packages/flutter_timeline_view/pubspec.yaml index 96ade42..01cf972 100644 --- a/packages/flutter_timeline_view/pubspec.yaml +++ b/packages/flutter_timeline_view/pubspec.yaml @@ -19,11 +19,13 @@ dependencies: dotted_border: ^2.1.0 flutter_html: ^3.0.0-beta.2 + # flutter_timeline_interface: + # git: + # url: https://github.com/Iconica-Development/flutter_timeline + # path: packages/flutter_timeline_interface + # ref: 1.0.0 flutter_timeline_interface: - git: - url: https://github.com/Iconica-Development/flutter_timeline - path: packages/flutter_timeline_interface - ref: 1.0.0 + path: ../flutter_timeline_interface flutter_image_picker: git: url: https://github.com/Iconica-Development/flutter_image_picker From 06ea5f028164627a7d1ece92213dab29bc32531a Mon Sep 17 00:00:00 2001 From: Jacques Date: Thu, 18 Jan 2024 11:20:53 +0100 Subject: [PATCH 04/11] merge --- .../lib/src/config/timeline_options.dart | 8 ++ .../lib/src/screens/timeline_screen.dart | 76 ++++++++++--------- .../lib/src/widgets/timeline_post_widget.dart | 6 +- 3 files changed, 50 insertions(+), 40 deletions(-) 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 5972a49..f5dfcdd 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -39,6 +39,8 @@ class TimelineOptions { this.nameBuilder, this.padding = const EdgeInsets.symmetric(vertical: 12.0), this.iconSize = 26, + this.postWidgetheight, + this.postPadding = const EdgeInsets.all(12.0), }); /// Theming options for the timeline @@ -105,6 +107,12 @@ class TimelineOptions { /// Size of icons like the comment and like icons. Dafualts to 26 final double iconSize; + + /// Sets a predefined height for the postWidget. + final double? postWidgetheight; + + /// Padding of each post + final EdgeInsets postPadding; } 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 d8f9a10..e025a9b 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,7 @@ class TimelineScreen extends StatefulWidget { this.onUserTap, this.posts, this.timelineCategoryFilter, + this.postWidget, super.key, }); @@ -30,7 +31,7 @@ class TimelineScreen extends StatefulWidget { /// All the configuration options for the timelinescreens and widgets final TimelineOptions options; - /// The controller for the scroll view + /// The controller for the scroll view final ScrollController? scrollController; /// The string to filter the timeline by category @@ -46,6 +47,9 @@ 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)? postWidget; + @override State createState() => _TimelineScreenState(); } @@ -95,43 +99,43 @@ class _TimelineScreenState extends State { children: [ ...posts.map( (post) => Padding( - padding: widget.options.padding, - child: TimelinePostWidget( - service: widget.service, - userId: widget.userId, - options: widget.options, - post: post, - height: widget.options.timelinePostHeight, - onTap: () async { - if (widget.onPostTap != null) { - widget.onPostTap!.call(post); - return; - } + padding: widget.options.postPadding, + child: widget.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); - }, + 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 => - service.likePost(widget.userId, post), - onTapUnlike: () async => - service.unlikePost(widget.userId, post), - onPostDelete: () async => service.deletePost(post), - onUserTap: widget.onUserTap, - ), + ); + }, + onTapLike: () async => + service.likePost(widget.userId, post), + onTapUnlike: () async => + service.unlikePost(widget.userId, post), + onPostDelete: () async => service.deletePost(post), + onUserTap: widget.onUserTap, + ), ), ), if (posts.isEmpty) diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index ea86de1..06ec2ae 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -13,7 +13,6 @@ class TimelinePostWidget extends StatefulWidget { required this.userId, required this.options, required this.post, - required this.height, required this.onTap, required this.onTapLike, required this.onTapUnlike, @@ -30,7 +29,6 @@ class TimelinePostWidget extends StatefulWidget { final TimelinePost post; /// Optional max height of the post - final double? height; final VoidCallback onTap; final VoidCallback onTapLike; final VoidCallback onTapUnlike; @@ -51,7 +49,7 @@ class _TimelinePostWidgetState extends State { return InkWell( onTap: widget.onTap, child: SizedBox( - height: widget.post.imageUrl != null ? widget.height : null, + height: widget.post.imageUrl != null ? widget.options.postWidgetheight : null, width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -145,7 +143,7 @@ class _TimelinePostWidgetState extends State { if (widget.post.imageUrl != null) ...[ const SizedBox(height: 8), Flexible( - flex: widget.height != null ? 1 : 0, + flex: widget.options.postWidgetheight != null ? 1 : 0, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: widget.options.doubleTapTolike From 14dced5ef4d856c629bf466ab58c6197bf5696ec Mon Sep 17 00:00:00 2001 From: Jacques Date: Thu, 18 Jan 2024 16:08:41 +0100 Subject: [PATCH 05/11] feat(category): Add the category selector --- .../lib/src/config/timeline_options.dart | 19 +++ .../lib/src/screens/timeline_screen.dart | 139 ++++++++++-------- .../lib/src/widgets/category_selector.dart | 71 +++++++++ .../src/widgets/category_selector_button.dart | 52 +++++++ 4 files changed, 221 insertions(+), 60 deletions(-) create mode 100644 packages/flutter_timeline_view/lib/src/widgets/category_selector.dart create mode 100644 packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart 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 f5dfcdd..d68c3a5 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -41,6 +41,9 @@ class TimelineOptions { this.iconSize = 26, this.postWidgetheight, this.postPadding = const EdgeInsets.all(12.0), + this.categories, + this.categoryButtonBuilder, + this.catergoryLabelBuilder, }); /// Theming options for the timeline @@ -113,6 +116,22 @@ class TimelineOptions { /// Padding of each post final EdgeInsets postPadding; + + /// List of categories that the user can select. + /// If this is null no categories will be shown. + final List? categories; + + /// Abilty to override the standard category selector + final Widget Function({ + required String? categoryKey, + required String categoryName, + required Function onTap, + required bool selected, + })? categoryButtonBuilder; + + /// Ability to set an proper label for the category selectors. + /// Default to category key. + final String Function(String? categoryKey)? catergoryLabelBuilder; } 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 e025a9b..b71b15e 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -7,6 +7,7 @@ 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({ @@ -60,6 +61,8 @@ class _TimelineScreenState extends State { bool isLoading = true; + late var filter = widget.timelineCategoryFilter; + @override void initState() { super.initState(); @@ -77,13 +80,10 @@ class _TimelineScreenState extends State { return ListenableBuilder( listenable: service, builder: (context, _) { - var posts = - widget.posts ?? service.getPosts(widget.timelineCategoryFilter); + var posts = widget.posts ?? service.getPosts(filter); posts = posts .where( - (p) => - widget.timelineCategoryFilter == null || - p.category == widget.timelineCategoryFilter, + (p) => filter == null || p.category == filter, ) .toList(); @@ -93,65 +93,84 @@ class _TimelineScreenState extends State { ? a.createdAt.compareTo(b.createdAt) : b.createdAt.compareTo(a.createdAt), ); - return SingleChildScrollView( - controller: controller, - child: Column( - children: [ - ...posts.map( - (post) => Padding( - padding: widget.options.postPadding, - child: widget.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); - }, - ), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CategorySelector( + filter: filter, + options: widget.options, + onTapCategory: (categoryKey) { + setState(() { + filter = categoryKey; + }); + }, + ), + Expanded( + child: SingleChildScrollView( + controller: controller, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...posts.map( + (post) => Padding( + padding: widget.options.postPadding, + child: widget.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); + }, + ), + ), + ), + ); + }, + onTapLike: () async => + service.likePost(widget.userId, post), + onTapUnlike: () async => + service.unlikePost(widget.userId, post), + onPostDelete: () async => + service.deletePost(post), + onUserTap: widget.onUserTap, ), - ); - }, - onTapLike: () async => - service.likePost(widget.userId, post), - onTapUnlike: () async => - service.unlikePost(widget.userId, post), - onPostDelete: () async => service.deletePost(post), - onUserTap: widget.onUserTap, ), + ), + if (posts.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + filter == null + ? widget.options.translations.noPosts + : widget.options.translations.noPostsWithFilter, + style: widget.options.theme.textStyles.noPostsStyle, + ), + ), + ), + ], ), ), - if (posts.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - widget.timelineCategoryFilter == null - ? widget.options.translations.noPosts - : widget.options.translations.noPostsWithFilter, - style: widget.options.theme.textStyles.noPostsStyle, - ), - ), - ), - ], - ), + ), + ], ); }, ); @@ -160,7 +179,7 @@ class _TimelineScreenState extends State { Future loadPosts() async { if (widget.posts != null) return; try { - await service.fetchPosts(widget.timelineCategoryFilter); + await service.fetchPosts(filter); setState(() { isLoading = false; }); diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart new file mode 100644 index 0000000..bed2303 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart @@ -0,0 +1,71 @@ +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({ + required this.filter, + required this.options, + required this.onTapCategory, + super.key, + }); + + final String? filter; + final TimelineOptions options; + final void Function(String? categoryKey) onTapCategory; + + @override + Widget build(BuildContext context) { + if (options.categories == null) { + return const SizedBox.shrink(); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: options.padding.horizontal, + ), + child: Row( + children: [ + 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, + ) ?? + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: CategorySelectorButton( + category: category, + selected: filter == category, + onTap: () => onTapCategory(category), + labelBuilder: options.catergoryLabelBuilder, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart new file mode 100644 index 0000000..cf0ae7d --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class CategorySelectorButton extends StatelessWidget { + const CategorySelectorButton({ + required this.category, + required this.selected, + required this.onTap, + this.labelBuilder, + super.key, + }); + + final String? category; + final bool selected; + final String Function(String? category)? labelBuilder; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return TextButton( + onPressed: onTap, + style: ButtonStyle( + padding: const MaterialStatePropertyAll( + EdgeInsets.symmetric( + vertical: 5, + horizontal: 12, + ), + ), + minimumSize: const MaterialStatePropertyAll(Size.zero), + backgroundColor: MaterialStatePropertyAll( + selected ? theme.colorScheme.primary : theme.colorScheme.surface, + ), + shape: const MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(45), + ), + ), + ), + ), + child: Text( + labelBuilder?.call(category) ?? category ?? 'All', + style: theme.textTheme.labelMedium?.copyWith( + color: selected + ? theme.colorScheme.onPrimary + : theme.colorScheme.onSurface, + ), + ), + ); + } +} From e5e2eb5c224578b5246b078f3a46f5b2d8506caa Mon Sep 17 00:00:00 2001 From: Jacques Date: Tue, 23 Jan 2024 11:22:51 +0100 Subject: [PATCH 06/11] feat: Added search bar with filter --- .../example/lib/post_screen.dart | 2 +- .../example/lib/timeline_service.dart | 31 +++-- .../lib/src/flutter_timeline_userstory.dart | 1 - .../service/firebase_timeline_service.dart | 54 ++++----- .../lib/flutter_timeline_interface.dart | 1 + .../lib/src/services/filter_service.dart | 22 ++++ .../lib/src/services/timeline_service.dart | 2 + .../lib/src/config/timeline_options.dart | 28 ++++- .../lib/src/config/timeline_translations.dart | 8 +- .../lib/src/screens/timeline_screen.dart | 112 +++++++++++++++--- .../lib/src/widgets/category_selector.dart | 72 +++++------ .../src/widgets/category_selector_button.dart | 1 + .../lib/src/widgets/timeline_post_widget.dart | 4 +- 13 files changed, 237 insertions(+), 101 deletions(-) create mode 100644 packages/flutter_timeline_interface/lib/src/services/filter_service.dart diff --git a/packages/flutter_timeline/example/lib/post_screen.dart b/packages/flutter_timeline/example/lib/post_screen.dart index b72369d..3c5f05a 100644 --- a/packages/flutter_timeline/example/lib/post_screen.dart +++ b/packages/flutter_timeline/example/lib/post_screen.dart @@ -22,7 +22,7 @@ class _PostScreenState extends State { body: TimelinePostScreen( userId: 'test_user', service: widget.service, - options: const TimelineOptions(), + options: TimelineOptions(), post: widget.post, onPostDelete: () { print('delete post'); diff --git a/packages/flutter_timeline/example/lib/timeline_service.dart b/packages/flutter_timeline/example/lib/timeline_service.dart index f4e2099..fe69001 100644 --- a/packages/flutter_timeline/example/lib/timeline_service.dart +++ b/packages/flutter_timeline/example/lib/timeline_service.dart @@ -11,11 +11,12 @@ import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:uuid/uuid.dart'; class TestTimelineService with ChangeNotifier implements TimelineService { - List _posts = []; + @override + List posts = []; @override Future createPost(TimelinePost post) async { - _posts.add( + posts.add( post.copyWith( creator: const TimelinePosterUserModel(userId: 'test_user'), ), @@ -26,7 +27,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future deletePost(TimelinePost post) async { - _posts = _posts.where((element) => element.id != post.id).toList(); + posts = posts.where((element) => element.id != post.id).toList(); notifyListeners(); } @@ -43,7 +44,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { reaction: post.reaction - 1, reactions: (post.reactions ?? [])..remove(reaction), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) @@ -64,7 +65,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { creator: const TimelinePosterUserModel(userId: 'test_user'))); } var updatedPost = post.copyWith(reactions: updatedReactions); - _posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); + posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); notifyListeners(); return updatedPost; } @@ -72,7 +73,6 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future> fetchPosts(String? category) async { var posts = getMockedPosts(); - _posts = posts; notifyListeners(); return posts; } @@ -83,7 +83,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { int limit, ) async { notifyListeners(); - return _posts; + return posts; } @override @@ -94,32 +94,31 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future> refreshPosts(String? category) async { - var posts = []; + var newPosts = []; - _posts = [...posts, ..._posts]; + posts = [...posts, ...newPosts]; notifyListeners(); return posts; } @override TimelinePost? getPost(String postId) => - (_posts.any((element) => element.id == postId)) - ? _posts.firstWhere((element) => element.id == postId) + (posts.any((element) => element.id == postId)) + ? posts.firstWhere((element) => element.id == postId) : null; @override - List getPosts(String? category) => _posts + List getPosts(String? category) => posts .where((element) => category == null || element.category == category) .toList(); @override Future likePost(String userId, TimelinePost post) async { - print(userId); var updatedPost = post.copyWith( likes: post.likes + 1, likedBy: (post.likedBy ?? [])..add(userId), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) @@ -135,7 +134,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { likes: post.likes - 1, likedBy: post.likedBy?..remove(userId), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) @@ -161,7 +160,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { reactions: post.reactions?..add(updatedReaction), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index b666a73..ffb4612 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -25,7 +25,6 @@ List getTimelineStoryRoutes( options: configuration.optionsBuilder(context), onPostTap: (post) async => TimelineUserStoryRoutes.timelineViewPath(post.id), - timelineCategoryFilter: null, ); return buildScreenWithoutTransition( context: context, diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart index d171c34..b74ff5d 100644 --- a/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_timeline_service.dart @@ -13,9 +13,7 @@ import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:uuid/uuid.dart'; -class FirebaseTimelineService - with ChangeNotifier - implements TimelineService, TimelineUserService { +class FirebaseTimelineService extends TimelineService with TimelineUserService { FirebaseTimelineService({ required TimelineUserService userService, FirebaseApp? app, @@ -35,8 +33,6 @@ class FirebaseTimelineService final Map _users = {}; - List _posts = []; - @override Future createPost(TimelinePost post) async { var postId = const Uuid().v4(); @@ -52,14 +48,14 @@ class FirebaseTimelineService var postRef = _db.collection(_options.timelineCollectionName).doc(updatedPost.id); await postRef.set(updatedPost.toJson()); - _posts.add(updatedPost); + posts.add(updatedPost); notifyListeners(); return updatedPost; } @override Future deletePost(TimelinePost post) async { - _posts = _posts.where((element) => element.id != post.id).toList(); + posts = posts.where((element) => element.id != post.id).toList(); var postRef = _db.collection(_options.timelineCollectionName).doc(post.id); await postRef.delete(); notifyListeners(); @@ -77,7 +73,7 @@ class FirebaseTimelineService reaction: post.reaction - 1, reactions: (post.reactions ?? [])..remove(reaction), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) @@ -107,7 +103,7 @@ class FirebaseTimelineService } } var updatedPost = post.copyWith(reactions: updatedReactions); - _posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); + posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); notifyListeners(); return updatedPost; } @@ -129,7 +125,7 @@ class FirebaseTimelineService var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user); posts.add(post); } - _posts = posts; + notifyListeners(); return posts; } @@ -140,12 +136,12 @@ class FirebaseTimelineService int limit, ) async { // only take posts that are in our category - var oldestPost = _posts + var oldestPost = posts .where( (element) => category == null || element.category == category, ) .fold( - _posts.first, + posts.first, (previousValue, element) => (previousValue.createdAt.isBefore(element.createdAt)) ? previousValue @@ -166,16 +162,16 @@ class FirebaseTimelineService .limit(limit) .get(); // add the new posts to the list - var posts = []; + var newPosts = []; for (var doc in snapshot.docs) { var data = doc.data(); var user = await _userService.getUser(data['creator_id']); var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user); - posts.add(post); + newPosts.add(post); } - _posts = [..._posts, ...posts]; + posts = [...posts, ...newPosts]; notifyListeners(); - return posts; + return newPosts; } @override @@ -190,7 +186,7 @@ class FirebaseTimelineService var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith( creator: user, ); - _posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); + posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); notifyListeners(); return updatedPost; } @@ -198,12 +194,12 @@ class FirebaseTimelineService @override Future> refreshPosts(String? category) async { // fetch all posts between now and the newest posts we have - var newestPostWeHave = _posts + var newestPostWeHave = posts .where( (element) => category == null || element.category == category, ) .fold( - _posts.first, + posts.first, (previousValue, element) => (previousValue.createdAt.isAfter(element.createdAt)) ? previousValue @@ -220,26 +216,26 @@ class FirebaseTimelineService .orderBy('created_at', descending: true) .endBefore([newestPostWeHave.createdAt]).get(); // add the new posts to the list - var posts = []; + var newPosts = []; for (var doc in snapshot.docs) { var data = doc.data(); var user = await _userService.getUser(data['creator_id']); var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user); - posts.add(post); + newPosts.add(post); } - _posts = [...posts, ..._posts]; + posts = [...posts, ...newPosts]; notifyListeners(); - return posts; + return newPosts; } @override TimelinePost? getPost(String postId) => - (_posts.any((element) => element.id == postId)) - ? _posts.firstWhere((element) => element.id == postId) + (posts.any((element) => element.id == postId)) + ? posts.firstWhere((element) => element.id == postId) : null; @override - List getPosts(String? category) => _posts + List getPosts(String? category) => posts .where((element) => category == null || element.category == category) .toList(); @@ -250,7 +246,7 @@ class FirebaseTimelineService likes: post.likes + 1, likedBy: post.likedBy?..add(userId), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) @@ -271,7 +267,7 @@ class FirebaseTimelineService likes: post.likes - 1, likedBy: post.likedBy?..remove(userId), ); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) @@ -314,7 +310,7 @@ class FirebaseTimelineService 'reaction': FieldValue.increment(1), 'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]), }); - _posts = _posts + posts = posts .map( (p) => p.id == post.id ? updatedPost : p, ) diff --git a/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart b/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart index 9676ad0..d0da25d 100644 --- a/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart +++ b/packages/flutter_timeline_interface/lib/flutter_timeline_interface.dart @@ -8,5 +8,6 @@ export 'src/model/timeline_category.dart'; export 'src/model/timeline_post.dart'; export 'src/model/timeline_poster.dart'; export 'src/model/timeline_reaction.dart'; +export 'src/services/filter_service.dart'; export 'src/services/timeline_service.dart'; export 'src/services/user_service.dart'; diff --git a/packages/flutter_timeline_interface/lib/src/services/filter_service.dart b/packages/flutter_timeline_interface/lib/src/services/filter_service.dart new file mode 100644 index 0000000..dc3441f --- /dev/null +++ b/packages/flutter_timeline_interface/lib/src/services/filter_service.dart @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; + +mixin TimelineFilterService on TimelineService { + List filterPosts( + String filterWord, + Map options, + ) { + var filteredPosts = posts + .where( + (post) => post.title.toLowerCase().contains( + filterWord.toLowerCase(), + ), + ) + .toList(); + + return filteredPosts; + } +} diff --git a/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart b/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart index d872eb2..6f4907d 100644 --- a/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart +++ b/packages/flutter_timeline_interface/lib/src/services/timeline_service.dart @@ -9,6 +9,8 @@ import 'package:flutter_timeline_interface/src/model/timeline_post.dart'; import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart'; abstract class TimelineService with ChangeNotifier { + List posts = []; + Future deletePost(TimelinePost post); Future deletePostReaction(TimelinePost post, String reactionId); Future createPost(TimelinePost post); 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 d68c3a5..1acfaba 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -8,9 +8,8 @@ import 'package:flutter_timeline_view/src/config/timeline_theme.dart'; import 'package:flutter_timeline_view/src/config/timeline_translations.dart'; import 'package:intl/intl.dart'; -@immutable class TimelineOptions { - const TimelineOptions({ + TimelineOptions({ this.theme = const TimelineTheme(), this.translations = const TimelineTranslations.empty(), this.imagePickerConfig = const ImagePickerConfig(), @@ -18,7 +17,7 @@ class TimelineOptions { this.timelinePostHeight, this.allowAllDeletion = false, this.sortCommentsAscending = true, - this.sortPostsAscending = false, + this.sortPostsAscending, this.doubleTapTolike = false, this.iconsWithValues = false, this.likeAndDislikeIconsForDoubleTap = const ( @@ -44,6 +43,10 @@ class TimelineOptions { this.categories, this.categoryButtonBuilder, this.catergoryLabelBuilder, + this.categorySelectorHorizontalPadding, + this.filterEnabled = false, + this.initialFilterWord, + this.searchBarBuilder, }); /// Theming options for the timeline @@ -59,7 +62,7 @@ class TimelineOptions { final bool sortCommentsAscending; /// Whether to sort posts ascending or descending - final bool sortPostsAscending; + final bool? sortPostsAscending; /// Allow all posts to be deleted instead of /// only the posts of the current user @@ -132,6 +135,23 @@ class TimelineOptions { /// 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; + + /// if true the filter textfield is enabled. + bool filterEnabled; + + /// Set a value to search through posts. When set the searchbar is shown. + /// If null no searchbar is shown. + final String? initialFilterWord; + + final Widget Function( + Future> Function( + String filterWord, + Map options, + ) search, + )? searchBarBuilder; } typedef ButtonBuilder = Widget Function( 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 f01b1bc..f43a3a4 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_translations.dart @@ -28,6 +28,7 @@ class TimelineTranslations { required this.postAt, required this.postLoadingError, required this.timelineSelectionDescription, + required this.searchHint, }); const TimelineTranslations.empty() @@ -52,7 +53,8 @@ class TimelineTranslations { writeComment = 'Write your comment here...', postAt = 'at', postLoadingError = 'Something went wrong while loading the post', - timelineSelectionDescription = 'Choose a category'; + timelineSelectionDescription = 'Choose a category', + searchHint = 'Search...'; final String noPosts; final String noPostsWithFilter; @@ -79,6 +81,8 @@ class TimelineTranslations { final String timelineSelectionDescription; + final String searchHint; + TimelineTranslations copyWith({ String? noPosts, String? noPostsWithFilter, @@ -101,6 +105,7 @@ class TimelineTranslations { String? firstComment, String? postLoadingError, String? timelineSelectionDescription, + String? searchHint, }) => TimelineTranslations( noPosts: noPosts ?? this.noPosts, @@ -127,5 +132,6 @@ class TimelineTranslations { postLoadingError: postLoadingError ?? this.postLoadingError, timelineSelectionDescription: timelineSelectionDescription ?? this.timelineSelectionDescription, + searchHint: searchHint ?? this.searchHint, ); } 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 b71b15e..cf8b8b7 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -18,7 +18,7 @@ class TimelineScreen extends StatefulWidget { this.onPostTap, this.onUserTap, this.posts, - this.timelineCategoryFilter, + this.timelineCategory, this.postWidget, super.key, }); @@ -36,7 +36,7 @@ class TimelineScreen extends StatefulWidget { final ScrollController? scrollController; /// The string to filter the timeline by category - final String? timelineCategoryFilter; + final String? timelineCategory; /// This is used if you want to pass in a list of posts instead /// of fetching them from the service @@ -57,11 +57,15 @@ class TimelineScreen extends StatefulWidget { class _TimelineScreenState extends State { late ScrollController controller; + late var textFieldController = + TextEditingController(text: widget.options.initialFilterWord); late var service = widget.service; bool isLoading = true; - late var filter = widget.timelineCategoryFilter; + late var category = widget.timelineCategory; + + late var filterWord = widget.options.initialFilterWord; @override void initState() { @@ -80,32 +84,109 @@ class _TimelineScreenState extends State { return ListenableBuilder( listenable: service, builder: (context, _) { - var posts = widget.posts ?? service.getPosts(filter); + var posts = widget.posts ?? service.getPosts(category); + posts = posts .where( - (p) => filter == null || p.category == filter, + (p) => category == null || p.category == category, ) .toList(); + if (widget.options.filterEnabled && filterWord != null) { + if (service is TimelineFilterService?) { + posts = + (service as TimelineFilterService).filterPosts(filterWord!, {}); + } else { + debugPrint('Timeline service needs to mixin' + ' with TimelineFilterService'); + } + } + // sort posts by date - posts.sort( - (a, b) => widget.options.sortPostsAscending - ? a.createdAt.compareTo(b.createdAt) - : b.createdAt.compareTo(a.createdAt), - ); + if (widget.options.sortPostsAscending != null) { + posts.sort( + (a, b) => widget.options.sortPostsAscending! + ? a.createdAt.compareTo(b.createdAt) + : b.createdAt.compareTo(a.createdAt), + ); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: widget.options.padding.top, + ), + if (widget.options.filterEnabled) ...[ + Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.options.padding.horizontal, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: TextField( + controller: textFieldController, + onChanged: (value) { + setState(() { + filterWord = value; + }); + }, + decoration: InputDecoration( + hintText: widget.options.translations.searchHint, + suffixIconConstraints: + const BoxConstraints(maxHeight: 14), + contentPadding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: -10, + ), + suffixIcon: const Padding( + padding: EdgeInsets.only(right: 12), + child: Icon(Icons.search), + ), + ), + ), + ), + const SizedBox( + width: 8, + ), + InkWell( + onTap: () { + setState(() { + textFieldController.clear(); + widget.options.filterEnabled = false; + filterWord = null; + }); + }, + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon( + Icons.close, + color: Color(0xFF000000), + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + ], CategorySelector( - filter: filter, + filter: category, options: widget.options, onTapCategory: (categoryKey) { setState(() { - filter = categoryKey; + category = categoryKey; }); }, ), + const SizedBox( + height: 12, + ), Expanded( child: SingleChildScrollView( controller: controller, @@ -159,7 +240,7 @@ class _TimelineScreenState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Text( - filter == null + category == null ? widget.options.translations.noPosts : widget.options.translations.noPostsWithFilter, style: widget.options.theme.textStyles.noPostsStyle, @@ -170,6 +251,9 @@ class _TimelineScreenState extends State { ), ), ), + SizedBox( + height: widget.options.padding.bottom, + ), ], ); }, @@ -179,7 +263,7 @@ class _TimelineScreenState extends State { Future loadPosts() async { if (widget.posts != null) return; try { - await service.fetchPosts(filter); + await service.fetchPosts(category); setState(() { isLoading = false; }); 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 bed2303..817090a 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart @@ -1,3 +1,5 @@ +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'; @@ -22,49 +24,51 @@ class CategorySelector extends StatelessWidget { return SingleChildScrollView( scrollDirection: Axis.horizontal, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: options.padding.horizontal, - ), - child: Row( - children: [ - options.categoryButtonBuilder?.call( - categoryKey: null, - categoryName: - options.catergoryLabelBuilder?.call(null) ?? 'All', - onTap: () => onTapCategory(null), + child: Row( + children: [ + SizedBox( + width: options.categorySelectorHorizontalPadding ?? + max(options.padding.horizontal - 4, 0), + ), + 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, ) ?? Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: CategorySelectorButton( - category: null, - selected: filter == null, - onTap: () => onTapCategory(null), + category: category, + selected: filter == category, + onTap: () => onTapCategory(category), 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, - ) ?? - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: CategorySelectorButton( - category: category, - selected: filter == category, - onTap: () => onTapCategory(category), - labelBuilder: options.catergoryLabelBuilder, - ), - ), - ], ], - ), + SizedBox( + width: options.categorySelectorHorizontalPadding ?? + max(options.padding.horizontal - 4, 0), + ), + ], ), ); } diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart index cf0ae7d..8b1a5db 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart @@ -21,6 +21,7 @@ class CategorySelectorButton extends StatelessWidget { return TextButton( onPressed: onTap, style: ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const MaterialStatePropertyAll( EdgeInsets.symmetric( vertical: 5, diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 06ec2ae..5c50d31 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -49,7 +49,9 @@ class _TimelinePostWidgetState extends State { return InkWell( onTap: widget.onTap, child: SizedBox( - height: widget.post.imageUrl != null ? widget.options.postWidgetheight : null, + height: widget.post.imageUrl != null + ? widget.options.postWidgetheight + : null, width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, From 9125c47ac4d6c45c269632926f5f2357236aed83 Mon Sep 17 00:00:00 2001 From: Jacques Date: Tue, 23 Jan 2024 14:01:43 +0100 Subject: [PATCH 07/11] fix: Fix some issues liek go router config --- README.md | 33 +++++++++++ .../example/lib/post_screen.dart | 2 +- .../lib/src/flutter_timeline_userstory.dart | 3 - .../src/models/timeline_configuration.dart | 2 +- .../lib/flutter_timeline_firebase.dart | 1 + .../src/service/firebase_user_service.dart | 55 +++++++++++++++++++ 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart diff --git a/README.md b/README.md index b63e679..b465115 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,39 @@ If you are going to use Firebase as the back-end of the Timeline, you should als ## How to use To use the module within your Flutter-application with predefined `Go_router` routes you should add the following: +Add go_router as dependency to your project. +Add the following configuration to your flutter_application: + +``` +List getTimelineStoryRoutes() => getTimelineStoryRoutes( + TimelineUserStoryConfiguration( + service: FirebaseTimelineService(), + userService: FirebaseUserService(), + userId: currentUserId, + categoriesBuilder: (context) {}, + optionsBuilder: (context) {}, + ), + ); +``` + +Add the `getTimelineStoryRoutes()` to your go_router routes like so: + +``` +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const MyHomePage( + title: "home", + ); + }, + ), + ...getTimelineStoryRoutes() + ], +); +``` + To add the `TimelineScreen` add the following code: ```` diff --git a/packages/flutter_timeline/example/lib/post_screen.dart b/packages/flutter_timeline/example/lib/post_screen.dart index 3c5f05a..0bae2fb 100644 --- a/packages/flutter_timeline/example/lib/post_screen.dart +++ b/packages/flutter_timeline/example/lib/post_screen.dart @@ -25,7 +25,7 @@ class _PostScreenState extends State { options: TimelineOptions(), post: widget.post, onPostDelete: () { - print('delete post'); + Navigator.of(context).pop(); }, ), ); diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index ffb4612..5ca037f 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -16,8 +16,6 @@ List getTimelineStoryRoutes( GoRoute( path: TimelineUserStoryRoutes.timelineHome, pageBuilder: (context, state) { - var timelineFilter = - Container(); // TODO(anyone): create a filter widget var timelineScreen = TimelineScreen( userId: configuration.userId, onUserTap: (user) => configuration.onUserTap?.call(context, user), @@ -31,7 +29,6 @@ List getTimelineStoryRoutes( state: state, child: configuration.mainPageBuilder?.call( context, - timelineFilter, timelineScreen, ) ?? Scaffold( diff --git a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart index 748f641..dc11f3f 100644 --- a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart +++ b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart @@ -25,7 +25,7 @@ class TimelineUserStoryConfiguration { final Function(BuildContext context, String userId)? onUserTap; - final Widget Function(BuildContext context, Widget filterBar, Widget child)? + final Widget Function(BuildContext context, Widget child)? mainPageBuilder; final Widget Function( diff --git a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart index 8a0afdc..9ad1f86 100644 --- a/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart +++ b/packages/flutter_timeline_firebase/lib/flutter_timeline_firebase.dart @@ -7,3 +7,4 @@ library flutter_timeline_firebase; export 'src/config/firebase_timeline_options.dart'; export 'src/service/firebase_timeline_service.dart'; +export 'src/service/firebase_user_service.dart'; diff --git a/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart b/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart new file mode 100644 index 0000000..fb1da17 --- /dev/null +++ b/packages/flutter_timeline_firebase/lib/src/service/firebase_user_service.dart @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.dart'; +import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; + +class FirebaseUserService implements TimelineUserService { + FirebaseUserService({ + FirebaseApp? app, + options = const FirebaseTimelineOptions(), + }) { + var appInstance = app ?? Firebase.app(); + _db = FirebaseFirestore.instanceFor(app: appInstance); + _options = options; + } + + late FirebaseFirestore _db; + late FirebaseTimelineOptions _options; + + final Map _users = {}; + + CollectionReference get _userCollection => _db + .collection(_options.usersCollectionName) + .withConverter( + fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( + snapshot.data()!, + snapshot.id, + ), + toFirestore: (user, _) => user.toJson(), + ); + @override + Future getUser(String userId) async { + if (_users.containsKey(userId)) { + return _users[userId]!; + } + var data = (await _userCollection.doc(userId).get()).data(); + + var user = data == null + ? TimelinePosterUserModel(userId: userId) + : TimelinePosterUserModel( + userId: userId, + firstName: data.firstName, + lastName: data.lastName, + imageUrl: data.imageUrl, + ); + + _users[userId] = user; + + return user; + } +} From e99e81c907ac0b5fd5d56e848d6c995d386cc517 Mon Sep 17 00:00:00 2001 From: Jacques Date: Wed, 24 Jan 2024 16:43:49 +0100 Subject: [PATCH 08/11] feat: added go router and navigator user stories --- README.md | 2 +- .../example/lib/apps/go_router/app.dart | 37 +++++ .../example/lib/apps/navigator/app.dart | 71 ++++++++++ .../example/lib/apps/widgets/app.dart | 95 +++++++++++++ .../widgets/screens}/post_screen.dart | 0 .../example/lib/config/config.dart | 75 +++++++++++ .../flutter_timeline/example/lib/main.dart | 127 ++---------------- .../lib/{ => services}/timeline_service.dart | 4 +- .../flutter_timeline/example/pubspec.yaml | 1 + .../example/test/widget_test.dart | 30 ----- .../lib/flutter_timeline.dart | 1 + .../flutter_timeline_navigator_userstory.dart | 49 +++++++ .../lib/src/flutter_timeline_userstory.dart | 68 ++-------- .../src/models/timeline_configuration.dart | 35 ++--- packages/flutter_timeline/lib/src/routes.dart | 4 - .../lib/src/model/timeline_category.dart | 4 +- .../lib/src/model/timeline_post.dart | 4 +- .../lib/flutter_timeline_view.dart | 2 + .../lib/src/config/timeline_options.dart | 14 +- .../timeline_post_creation_screen.dart | 4 +- .../lib/src/screens/timeline_post_screen.dart | 51 +++++-- .../lib/src/screens/timeline_screen.dart | 35 +---- .../lib/src/widgets/category_selector.dart | 37 ++--- .../src/widgets/category_selector_button.dart | 7 +- 24 files changed, 437 insertions(+), 320 deletions(-) create mode 100644 packages/flutter_timeline/example/lib/apps/go_router/app.dart create mode 100644 packages/flutter_timeline/example/lib/apps/navigator/app.dart create mode 100644 packages/flutter_timeline/example/lib/apps/widgets/app.dart rename packages/flutter_timeline/example/lib/{ => apps/widgets/screens}/post_screen.dart (100%) create mode 100644 packages/flutter_timeline/example/lib/config/config.dart rename packages/flutter_timeline/example/lib/{ => services}/timeline_service.dart (98%) delete mode 100644 packages/flutter_timeline/example/test/widget_test.dart create mode 100644 packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart diff --git a/README.md b/README.md index b465115..5c29ace 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ final GoRouter _router = GoRouter( ); }, ), - ...getTimelineStoryRoutes() + ...getTimelineStoryRoutes(timelineUserStoryConfiguration) ], ); ``` diff --git a/packages/flutter_timeline/example/lib/apps/go_router/app.dart b/packages/flutter_timeline/example/lib/apps/go_router/app.dart new file mode 100644 index 0000000..ef18fef --- /dev/null +++ b/packages/flutter_timeline/example/lib/apps/go_router/app.dart @@ -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 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, + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/apps/navigator/app.dart b/packages/flutter_timeline/example/lib/apps/navigator/app.dart new file mode 100644 index 0000000..2473d64 --- /dev/null +++ b/packages/flutter_timeline/example/lib/apps/navigator/app.dart @@ -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 createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + 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), + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/apps/widgets/app.dart b/packages/flutter_timeline/example/lib/apps/widgets/app.dart new file mode 100644 index 0000000..8367742 --- /dev/null +++ b/packages/flutter_timeline/example/lib/apps/widgets/app.dart @@ -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 createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + 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(); + }, + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/packages/flutter_timeline/example/lib/post_screen.dart b/packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart similarity index 100% rename from packages/flutter_timeline/example/lib/post_screen.dart rename to packages/flutter_timeline/example/lib/apps/widgets/screens/post_screen.dart diff --git a/packages/flutter_timeline/example/lib/config/config.dart b/packages/flutter_timeline/example/lib/config/config.dart new file mode 100644 index 0000000..046615f --- /dev/null +++ b/packages/flutter_timeline/example/lib/config/config.dart @@ -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, + ), + ); +} diff --git a/packages/flutter_timeline/example/lib/main.dart b/packages/flutter_timeline/example/lib/main.dart index c383657..9f32ce9 100644 --- a/packages/flutter_timeline/example/lib/main.dart +++ b/packages/flutter_timeline/example/lib/main.dart @@ -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 createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - 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()); } diff --git a/packages/flutter_timeline/example/lib/timeline_service.dart b/packages/flutter_timeline/example/lib/services/timeline_service.dart similarity index 98% rename from packages/flutter_timeline/example/lib/timeline_service.dart rename to packages/flutter_timeline/example/lib/services/timeline_service.dart index fe69001..3788e41 100644 --- a/packages/flutter_timeline/example/lib/timeline_service.dart +++ b/packages/flutter_timeline/example/lib/services/timeline_service.dart @@ -72,7 +72,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService { @override Future> 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, diff --git a/packages/flutter_timeline/example/pubspec.yaml b/packages/flutter_timeline/example/pubspec.yaml index 27c1008..dd8dfa9 100644 --- a/packages/flutter_timeline/example/pubspec.yaml +++ b/packages/flutter_timeline/example/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_timeline: path: ../ intl: ^0.19.0 + go_router: ^13.0.1 dev_dependencies: flutter_test: diff --git a/packages/flutter_timeline/example/test/widget_test.dart b/packages/flutter_timeline/example/test/widget_test.dart deleted file mode 100644 index 092d222..0000000 --- a/packages/flutter_timeline/example/test/widget_test.dart +++ /dev/null @@ -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); - }); -} diff --git a/packages/flutter_timeline/lib/flutter_timeline.dart b/packages/flutter_timeline/lib/flutter_timeline.dart index b4af6ca..9c68eb6 100644 --- a/packages/flutter_timeline/lib/flutter_timeline.dart +++ b/packages/flutter_timeline/lib/flutter_timeline.dart @@ -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'; diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart new file mode 100644 index 0000000..cc375aa --- /dev/null +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -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); + }, + ); diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart index 5ca037f..347fcd2 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_userstory.dart @@ -22,12 +22,16 @@ List getTimelineStoryRoutes( service: configuration.service, options: configuration.optionsBuilder(context), onPostTap: (post) async => - TimelineUserStoryRoutes.timelineViewPath(post.id), + 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 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, diff --git a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart index dc11f3f..c06ce3f 100644 --- a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart +++ b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart @@ -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 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; } diff --git a/packages/flutter_timeline/lib/src/routes.dart b/packages/flutter_timeline/lib/src/routes.dart index 3196bea..b6c70e5 100644 --- a/packages/flutter_timeline/lib/src/routes.dart +++ b/packages/flutter_timeline/lib/src/routes.dart @@ -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'; } diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart index 430e14c..b88d1d8 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_category.dart @@ -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; diff --git a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart index 1e08d71..df7b7e9 100644 --- a/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart +++ b/packages/flutter_timeline_interface/lib/src/model/timeline_post.dart @@ -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; diff --git a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart index d0b2f63..88cdb20 100644 --- a/packages/flutter_timeline_view/lib/flutter_timeline_view.dart +++ b/packages/flutter_timeline_view/lib/flutter_timeline_view.dart @@ -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'; 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 1acfaba..5fc676f 100644 --- a/packages/flutter_timeline_view/lib/src/config/timeline_options.dart +++ b/packages/flutter_timeline_view/lib/src/config/timeline_options.dart @@ -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? categories; + final List 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 options, ) search, )? searchBarBuilder; + + /// Override the standard postwidget + final Widget Function(TimelinePost post)? postWidget; } typedef ButtonBuilder = Widget Function( 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 3e52ab9..2238f4e 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 @@ -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; diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index 4c9983b..467da65 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -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 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 { +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 { 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 { 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) => >[ PopupMenuItem( 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 cf8b8b7..9ed7db4 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -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? 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 createState() => _TimelineScreenState(); } @@ -196,35 +191,13 @@ class _TimelineScreenState extends State { ...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 => 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 817090a..f0d3cfc 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector.dart @@ -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), ), - 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!) ...[ + for (var category in 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), ), ), ], diff --git a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart index 8b1a5db..3245303 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/category_selector_button.dart @@ -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 From 73ac5086223292f93f3aa005d432c8d87860dd59 Mon Sep 17 00:00:00 2001 From: Jacques Date: Thu, 25 Jan 2024 13:36:56 +0100 Subject: [PATCH 09/11] fix: Refactor and fixes --- .../example/lib/config/config.dart | 36 ++++++------- .../flutter_timeline_navigator_userstory.dart | 2 + .../lib/src/flutter_timeline_userstory.dart | 2 + .../src/models/timeline_configuration.dart | 6 +++ .../lib/src/config/timeline_options.dart | 51 ++++++++++++++----- .../lib/src/screens/timeline_screen.dart | 36 ++++++++----- .../lib/src/widgets/category_selector.dart | 16 +++--- packages/flutter_timeline_view/pubspec.yaml | 1 + 8 files changed, 100 insertions(+), 50 deletions(-) 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 From dea258f5a53f39a6ed54db434093a30deb8af5cc Mon Sep 17 00:00:00 2001 From: Jacques Date: Thu, 25 Jan 2024 14:33:14 +0100 Subject: [PATCH 10/11] feat: readme --- README.md | 23 +++++++++++++++++- .../flutter_timeline/example/assets/logo.png | Bin 20506 -> 0 bytes .../flutter_timeline/example/pubspec.yaml | 4 +-- .../lib/src/config/timeline_options.dart | 6 +++-- .../lib/src/widgets/timeline_post_widget.dart | 4 +-- 5 files changed, 30 insertions(+), 7 deletions(-) delete mode 100644 packages/flutter_timeline/example/assets/logo.png diff --git a/README.md b/README.md index 5c29ace..83062cf 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ List getTimelineStoryRoutes() => getTimelineStoryRoutes( service: FirebaseTimelineService(), userService: FirebaseUserService(), userId: currentUserId, - categoriesBuilder: (context) {}, optionsBuilder: (context) {}, ), ); @@ -59,6 +58,15 @@ final GoRouter _router = GoRouter( ); ``` +The user story can also be used without go router: +Add the following code somewhere in your widget tree: + +```` +timeLineNavigatorUserStory(TimelineUserStoryConfiguration, context), +```` + + +Or create your own routing using the Screens: To add the `TimelineScreen` add the following code: ```` @@ -66,6 +74,7 @@ TimelineScreen( userId: currentUserId, service: timelineService, options: timelineOptions, + onPostTap: (post) {} ), ```` @@ -117,20 +126,32 @@ The `TimelineOptions` has its own parameters, as specified below: | Parameter | Explanation | |-----------|-------------| +| theme | Used to set icon colors and textstyles | | translations | Ability to provide desired text and tanslations. | +| imagePickerConfig | Config for the image picker in the post creation screen. | +| imagePickerTheme | Theme for the image picker in the post creation screen. | | timelinePostHeight | Sets the height for each post widget in the list of post. If null, the size depends on the size of the image. | | allowAllDeletion | Determines of users are allowed to delete thier own posts. | | sortCommentsAscending | Determines if the comments are sorted from old to new or new to old. | | sortPostsAscending | Determines if the posts are sorted from old to new or new to old. | +| doubleTapToLike | Enables the abilty to double tap the image to like the post. | +| iconsWithValues | Ability to provide desired text and tanslations. | +| likeAndDislikeIconsForDoubleTap | Ability to override the standard icon which appears on double tap. | +| itemInfoBuilder | Ability to override the bottom of the postwidgets. (Everything under the like and comment icons) | | dateFormat | Sets the used date format | | timeFormat | Sets the used time format | | buttonBuilder | The ability to provide a custom button for the post creation screen. | | textInputBuilder | The ability to provide a custom text input widget for the post creation screen. | +| dividerBuilder | Ability to provide desired text and tanslations. | | userAvatarBuilder | The ability to provide a custom avatar. | | anonymousAvatarBuilder | The ability to provide a custom avatar for anonymous users. | | nameBuilder | The ability to override the standard way of display the post creator name. | | padding | Padding used for the whole page. | | iconSize | Size of icons like the comment and like icons. Dafualts to 26. | +| postWidgetHeight | Ability to provide desired text and tanslations. | +| postPadding | Padding for each post. | +| filterOptions | Options for using the filter to filter posts. | +| categoriesOptions | Options for using the category selector to provide posts of a certain category. | The `ImagePickerTheme` ans `imagePickerConfig` also have their own parameters, how to use these parameters can be found in [the documentation of the flutter_image_picker package](https://github.com/Iconica-Development/flutter_image_picker). diff --git a/packages/flutter_timeline/example/assets/logo.png b/packages/flutter_timeline/example/assets/logo.png deleted file mode 100644 index 9a434e42e6a1785ffbedb83ea7dc39037433c382..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20506 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 { onTap: widget.onTap, child: SizedBox( height: widget.post.imageUrl != null - ? widget.options.postWidgetheight + ? widget.options.postWidgetHeight : null, width: double.infinity, child: Column( @@ -145,7 +145,7 @@ class _TimelinePostWidgetState extends State { if (widget.post.imageUrl != null) ...[ const SizedBox(height: 8), Flexible( - flex: widget.options.postWidgetheight != null ? 1 : 0, + flex: widget.options.postWidgetHeight != null ? 1 : 0, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: widget.options.doubleTapTolike From d075e35d7485d420717b8a62029b0faa1fa059c3 Mon Sep 17 00:00:00 2001 From: Jacques Date: Thu, 25 Jan 2024 14:49:49 +0100 Subject: [PATCH 11/11] chore: Update version and dependecies --- packages/flutter_timeline/pubspec.yaml | 20 +++++++++---------- .../flutter_timeline_firebase/pubspec.yaml | 11 +++++----- .../flutter_timeline_interface/pubspec.yaml | 2 +- packages/flutter_timeline_view/pubspec.yaml | 11 +++++----- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/flutter_timeline/pubspec.yaml b/packages/flutter_timeline/pubspec.yaml index 960cfec..83e93f5 100644 --- a/packages/flutter_timeline/pubspec.yaml +++ b/packages/flutter_timeline/pubspec.yaml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later name: flutter_timeline description: Visual elements and interface combined into one package -version: 1.0.0 +version: 2.0.0 publish_to: none @@ -16,18 +16,16 @@ dependencies: go_router: any flutter_timeline_view: - path: ../flutter_timeline_view - # git: - # url: https://github.com/Iconica-Development/flutter_timeline - # path: packages/flutter_timeline_view - # ref: 1.0.0 + git: + url: https://github.com/Iconica-Development/flutter_timeline + path: packages/flutter_timeline_view + ref: 2.0.0 flutter_timeline_interface: - path: ../flutter_timeline_interface - # git: - # url: https://github.com/Iconica-Development/flutter_timeline - # path: packages/flutter_timeline_interface - # ref: 1.0.0 + git: + url: https://github.com/Iconica-Development/flutter_timeline + path: packages/flutter_timeline_interface + ref: 2.0.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/packages/flutter_timeline_firebase/pubspec.yaml b/packages/flutter_timeline_firebase/pubspec.yaml index 517b9f5..cefbac5 100644 --- a/packages/flutter_timeline_firebase/pubspec.yaml +++ b/packages/flutter_timeline_firebase/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_timeline_firebase description: Implementation of the Flutter Timeline interface for Firebase. -version: 1.0.0 +version: 2.0.0 publish_to: none @@ -20,11 +20,10 @@ dependencies: uuid: ^4.2.1 flutter_timeline_interface: - path: ../flutter_timeline_interface - # git: - # url: https://github.com/Iconica-Development/flutter_timeline - # path: packages/flutter_timeline_interface - # ref: 1.0.0 + git: + url: https://github.com/Iconica-Development/flutter_timeline + path: packages/flutter_timeline_interface + ref: 2.0.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/packages/flutter_timeline_interface/pubspec.yaml b/packages/flutter_timeline_interface/pubspec.yaml index 051b746..0530663 100644 --- a/packages/flutter_timeline_interface/pubspec.yaml +++ b/packages/flutter_timeline_interface/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_timeline_interface description: Interface for the service of the Flutter Timeline component -version: 1.0.0 +version: 2.0.0 publish_to: none diff --git a/packages/flutter_timeline_view/pubspec.yaml b/packages/flutter_timeline_view/pubspec.yaml index 19fae1e..3b1cbce 100644 --- a/packages/flutter_timeline_view/pubspec.yaml +++ b/packages/flutter_timeline_view/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_timeline_view description: Visual elements of the Flutter Timeline Component -version: 1.0.0 +version: 2.0.0 publish_to: none @@ -20,11 +20,10 @@ dependencies: flutter_html: ^3.0.0-beta.2 flutter_timeline_interface: - path: ../flutter_timeline_interface - # git: - # url: https://github.com/Iconica-Development/flutter_timeline - # path: packages/flutter_timeline_interface - # ref: 1.0.0 + git: + url: https://github.com/Iconica-Development/flutter_timeline + path: packages/flutter_timeline_interface + ref: 2.0.0 flutter_image_picker: git: url: https://github.com/Iconica-Development/flutter_image_picker