mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
Merge pull request #9 from Iconica-Development/bugfix/feedback_fixes
Bugfix/feedback fixes
This commit is contained in:
commit
5c475d4de5
39 changed files with 1973 additions and 565 deletions
133
README.md
133
README.md
|
@ -1,5 +1,6 @@
|
||||||
# Flutter Timeline
|
# 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -23,7 +24,137 @@ If you are going to use Firebase as the back-end of the Timeline, you should als
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use
|
## 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:
|
||||||
|
|
||||||
|
Add go_router as dependency to your project.
|
||||||
|
Add the following configuration to your flutter_application:
|
||||||
|
|
||||||
|
```
|
||||||
|
List<GoRoute> getTimelineStoryRoutes() => getTimelineStoryRoutes(
|
||||||
|
TimelineUserStoryConfiguration(
|
||||||
|
service: FirebaseTimelineService(),
|
||||||
|
userService: FirebaseUserService(),
|
||||||
|
userId: currentUserId,
|
||||||
|
optionsBuilder: (context) {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the `getTimelineStoryRoutes()` to your go_router routes like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
final GoRouter _router = GoRouter(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const MyHomePage(
|
||||||
|
title: "home",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...getTimelineStoryRoutes(timelineUserStoryConfiguration)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
````
|
||||||
|
TimelineScreen(
|
||||||
|
userId: currentUserId,
|
||||||
|
service: timelineService,
|
||||||
|
options: timelineOptions,
|
||||||
|
onPostTap: (post) {}
|
||||||
|
),
|
||||||
|
````
|
||||||
|
|
||||||
|
`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 |
|
||||||
|
|-----------|-------------|
|
||||||
|
| 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).
|
||||||
|
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
|
@ -27,7 +27,6 @@ migrate_working_dir/
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.packages
|
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:example/config/config.dart';
|
||||||
|
import 'package:example/services/timeline_service.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
List<GoRoute> getTimelineRoutes() => getTimelineStoryRoutes(
|
||||||
|
getConfig(
|
||||||
|
TestTimelineService(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final _router = GoRouter(
|
||||||
|
initialLocation: '/timeline',
|
||||||
|
routes: [
|
||||||
|
...getTimelineRoutes(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
class GoRouterApp extends StatelessWidget {
|
||||||
|
const GoRouterApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
routerConfig: _router,
|
||||||
|
title: 'Flutter Timeline',
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme:
|
||||||
|
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
||||||
|
background: const Color(0xFFB8E2E8),
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:example/config/config.dart';
|
||||||
|
import 'package:example/services/timeline_service.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||||
|
|
||||||
|
class NavigatorApp extends StatelessWidget {
|
||||||
|
const NavigatorApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Flutter Timeline',
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme:
|
||||||
|
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
||||||
|
background: const Color(0xFFB8E2E8),
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: const MyHomePage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyHomePage extends StatefulWidget {
|
||||||
|
const MyHomePage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
var timelineService = TestTimelineService();
|
||||||
|
var timelineOptions = options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
floatingActionButton: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
FloatingActionButton(
|
||||||
|
heroTag: 'btn1',
|
||||||
|
onPressed: () =>
|
||||||
|
createPost(context, timelineService, timelineOptions),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
FloatingActionButton(
|
||||||
|
heroTag: 'btn2',
|
||||||
|
onPressed: () => generatePost(timelineService),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: timeLineNavigatorUserStory(getConfig(timelineService), context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
packages/flutter_timeline/example/lib/apps/widgets/app.dart
Normal file
95
packages/flutter_timeline/example/lib/apps/widgets/app.dart
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import 'package:example/config/config.dart';
|
||||||
|
import 'package:example/services/timeline_service.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||||
|
|
||||||
|
class WidgetApp extends StatelessWidget {
|
||||||
|
const WidgetApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Flutter Timeline',
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme:
|
||||||
|
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
||||||
|
background: const Color(0xFFB8E2E8),
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: const MyHomePage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyHomePage extends StatefulWidget {
|
||||||
|
const MyHomePage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
var timelineService = TestTimelineService();
|
||||||
|
var timelineOptions = options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
floatingActionButton: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
createPost(context, timelineService, timelineOptions);
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
generatePost(timelineService);
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: TimelineScreen(
|
||||||
|
userId: 'test_user',
|
||||||
|
service: timelineService,
|
||||||
|
options: timelineOptions,
|
||||||
|
onPostTap: (post) async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: TimelinePostScreen(
|
||||||
|
userId: 'test_user',
|
||||||
|
service: timelineService,
|
||||||
|
options: timelineOptions,
|
||||||
|
post: post,
|
||||||
|
onPostDelete: () {
|
||||||
|
timelineService.deletePost(post);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
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<PostScreen> createState() => _PostScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostScreenState extends State<PostScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: TimelinePostScreen(
|
||||||
|
userId: 'test_user',
|
||||||
|
service: widget.service,
|
||||||
|
options: TimelineOptions(),
|
||||||
|
post: widget.post,
|
||||||
|
onPostDelete: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestUserService implements TimelineUserService {
|
||||||
|
final Map<String, TimelinePosterUserModel> _users = {
|
||||||
|
'test_user': const TimelinePosterUserModel(userId: 'test_user')
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePosterUserModel?> getUser(String userId) async {
|
||||||
|
if (_users.containsKey(userId)) {
|
||||||
|
return _users[userId]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_users[userId] = TimelinePosterUserModel(userId: userId);
|
||||||
|
|
||||||
|
return TimelinePosterUserModel(userId: userId);
|
||||||
|
}
|
||||||
|
}
|
77
packages/flutter_timeline/example/lib/config/config.dart
Normal file
77
packages/flutter_timeline/example/lib/config/config.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
15
packages/flutter_timeline/example/lib/main.dart
Normal file
15
packages/flutter_timeline/example/lib/main.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// 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:intl/date_symbol_data_local.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
initializeDateFormatting();
|
||||||
|
|
||||||
|
// Uncomment any, but only one, of these lines to run the example with specific navigation.
|
||||||
|
|
||||||
|
runApp(const WidgetApp());
|
||||||
|
// runApp(const NavigatorApp());
|
||||||
|
// runApp(const GoRouterApp());
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
// 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 {
|
||||||
|
@override
|
||||||
|
List<TimelinePost> posts = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||||
|
posts.add(
|
||||||
|
post.copyWith(
|
||||||
|
creator: const TimelinePosterUserModel(userId: 'test_user'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deletePost(TimelinePost post) async {
|
||||||
|
posts = posts.where((element) => element.id != post.id).toList();
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> 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<TimelinePost> fetchPostDetails(TimelinePost post) async {
|
||||||
|
var reactions = post.reactions ?? [];
|
||||||
|
var updatedReactions = <TimelinePostReaction>[];
|
||||||
|
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<List<TimelinePost>> fetchPosts(String? category) async {
|
||||||
|
posts = getMockedPosts();
|
||||||
|
notifyListeners();
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<TimelinePost>> fetchPostsPaginated(
|
||||||
|
String? category,
|
||||||
|
int limit,
|
||||||
|
) async {
|
||||||
|
notifyListeners();
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> fetchPost(TimelinePost post) async {
|
||||||
|
notifyListeners();
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
||||||
|
var newPosts = <TimelinePost>[];
|
||||||
|
|
||||||
|
posts = [...posts, ...newPosts];
|
||||||
|
notifyListeners();
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelinePost? getPost(String postId) =>
|
||||||
|
(posts.any((element) => element.id == postId))
|
||||||
|
? posts.firstWhere((element) => element.id == postId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<TimelinePost> getPosts(String? category) => posts
|
||||||
|
.where((element) => category == null || element.category == category)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> 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<TimelinePost> 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<TimelinePost> 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<TimelinePost> getMockedPosts() {
|
||||||
|
return [
|
||||||
|
TimelinePost(
|
||||||
|
id: 'Post0',
|
||||||
|
creatorId: 'test_user',
|
||||||
|
title: 'Post 0',
|
||||||
|
category: null,
|
||||||
|
content: "Post 0 content",
|
||||||
|
likes: 0,
|
||||||
|
reaction: 0,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
reactionEnabled: false,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
93
packages/flutter_timeline/example/pubspec.yaml
Normal file
93
packages/flutter_timeline/example/pubspec.yaml
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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: ../
|
||||||
|
intl: ^0.19.0
|
||||||
|
go_router: ^13.0.1
|
||||||
|
|
||||||
|
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:
|
||||||
|
# - assets/
|
||||||
|
|
||||||
|
# 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
|
|
@ -5,6 +5,7 @@
|
||||||
/// Flutter Timeline library
|
/// Flutter Timeline library
|
||||||
library flutter_timeline;
|
library flutter_timeline;
|
||||||
|
|
||||||
|
export 'package:flutter_timeline/src/flutter_timeline_navigator_userstory.dart';
|
||||||
export 'package:flutter_timeline/src/flutter_timeline_userstory.dart';
|
export 'package:flutter_timeline/src/flutter_timeline_userstory.dart';
|
||||||
export 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
export 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
||||||
export 'package:flutter_timeline/src/routes.dart';
|
export 'package:flutter_timeline/src/routes.dart';
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
// 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);
|
||||||
|
},
|
||||||
|
filterEnabled: configuration.filterEnabled,
|
||||||
|
postWidgetBuilder: configuration.postWidgetBuilder,
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
|
@ -16,23 +16,25 @@ List<GoRoute> getTimelineStoryRoutes(
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: TimelineUserStoryRoutes.timelineHome,
|
path: TimelineUserStoryRoutes.timelineHome,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
var timelineFilter =
|
|
||||||
Container(); // TODO(anyone): create a filter widget
|
|
||||||
var timelineScreen = TimelineScreen(
|
var timelineScreen = TimelineScreen(
|
||||||
userId: configuration.userId,
|
userId: configuration.userId,
|
||||||
onUserTap: (user) => configuration.onUserTap?.call(context, user),
|
onUserTap: (user) => configuration.onUserTap?.call(context, user),
|
||||||
service: configuration.service,
|
service: configuration.service,
|
||||||
options: configuration.optionsBuilder(context),
|
options: configuration.optionsBuilder(context),
|
||||||
onPostTap: (post) async =>
|
onPostTap: (post) async =>
|
||||||
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
configuration.onPostTap?.call(context, post) ??
|
||||||
timelineCategoryFilter: null,
|
await context.push(
|
||||||
|
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
||||||
|
),
|
||||||
|
filterEnabled: configuration.filterEnabled,
|
||||||
|
postWidgetBuilder: configuration.postWidgetBuilder,
|
||||||
);
|
);
|
||||||
|
|
||||||
return buildScreenWithoutTransition(
|
return buildScreenWithoutTransition(
|
||||||
context: context,
|
context: context,
|
||||||
state: state,
|
state: state,
|
||||||
child: configuration.mainPageBuilder?.call(
|
child: configuration.openPageBuilder?.call(
|
||||||
context,
|
context,
|
||||||
timelineFilter,
|
|
||||||
timelineScreen,
|
timelineScreen,
|
||||||
) ??
|
) ??
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -41,74 +43,27 @@ List<GoRoute> getTimelineStoryRoutes(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: TimelineUserStoryRoutes.timelineSelect,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
var timelineSelectionWidget = TimelineSelectionScreen(
|
|
||||||
options: configuration.optionsBuilder(context),
|
|
||||||
categories: configuration.categoriesBuilder(context),
|
|
||||||
onCategorySelected: (category) async => context.push(
|
|
||||||
TimelineUserStoryRoutes.timelineCreatePath(category.name),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return buildScreenWithoutTransition(
|
|
||||||
context: context,
|
|
||||||
state: state,
|
|
||||||
child: configuration.postSelectionScreenBuilder?.call(
|
|
||||||
context,
|
|
||||||
timelineSelectionWidget,
|
|
||||||
) ??
|
|
||||||
Scaffold(
|
|
||||||
body: timelineSelectionWidget,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: TimelineUserStoryRoutes.timelineCreate,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
var timelineCreateWidget = TimelinePostCreationScreen(
|
|
||||||
userId: configuration.userId,
|
|
||||||
options: configuration.optionsBuilder(context),
|
|
||||||
postCategory: state.pathParameters['category'] ?? '',
|
|
||||||
service: configuration.service,
|
|
||||||
onPostCreated: (post) => context.go(
|
|
||||||
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return buildScreenWithoutTransition(
|
|
||||||
context: context,
|
|
||||||
state: state,
|
|
||||||
child: configuration.postCreationScreenBuilder?.call(
|
|
||||||
context,
|
|
||||||
timelineCreateWidget,
|
|
||||||
) ??
|
|
||||||
Scaffold(
|
|
||||||
body: timelineCreateWidget,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: TimelineUserStoryRoutes.timelineView,
|
path: TimelineUserStoryRoutes.timelineView,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
|
var post =
|
||||||
|
configuration.service.getPost(state.pathParameters['post']!)!;
|
||||||
|
|
||||||
var timelinePostWidget = TimelinePostScreen(
|
var timelinePostWidget = TimelinePostScreen(
|
||||||
userId: configuration.userId,
|
userId: configuration.userId,
|
||||||
options: configuration.optionsBuilder(context),
|
options: configuration.optionsBuilder(context),
|
||||||
service: configuration.service,
|
service: configuration.service,
|
||||||
userService: configuration.userService,
|
post: post,
|
||||||
post: configuration.service.getPost(state.pathParameters['post']!)!,
|
onPostDelete: () => configuration.onPostDelete?.call(context, post),
|
||||||
onPostDelete: () => context.pop(),
|
|
||||||
onUserTap: (user) => configuration.onUserTap?.call(context, user),
|
onUserTap: (user) => configuration.onUserTap?.call(context, user),
|
||||||
);
|
);
|
||||||
var category = configuration.categoriesBuilder(context).first;
|
|
||||||
return buildScreenWithoutTransition(
|
return buildScreenWithoutTransition(
|
||||||
context: context,
|
context: context,
|
||||||
state: state,
|
state: state,
|
||||||
child: configuration.postScreenBuilder?.call(
|
child: configuration.openPageBuilder?.call(
|
||||||
context,
|
context,
|
||||||
timelinePostWidget,
|
timelinePostWidget,
|
||||||
category,
|
|
||||||
) ??
|
) ??
|
||||||
Scaffold(
|
Scaffold(
|
||||||
body: timelinePostWidget,
|
body: timelinePostWidget,
|
||||||
|
|
|
@ -9,42 +9,35 @@ import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||||
@immutable
|
@immutable
|
||||||
class TimelineUserStoryConfiguration {
|
class TimelineUserStoryConfiguration {
|
||||||
const TimelineUserStoryConfiguration({
|
const TimelineUserStoryConfiguration({
|
||||||
required this.categoriesBuilder,
|
|
||||||
required this.optionsBuilder,
|
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.userService,
|
required this.userService,
|
||||||
this.mainPageBuilder,
|
required this.optionsBuilder,
|
||||||
this.postScreenBuilder,
|
this.openPageBuilder,
|
||||||
this.postCreationScreenBuilder,
|
this.onPostTap,
|
||||||
this.postSelectionScreenBuilder,
|
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
|
this.onPostDelete,
|
||||||
|
this.filterEnabled = false,
|
||||||
|
this.postWidgetBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
final Function(BuildContext context, String userId)? onUserTap;
|
|
||||||
|
|
||||||
final Widget Function(BuildContext context, Widget filterBar, Widget child)?
|
|
||||||
mainPageBuilder;
|
|
||||||
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
Widget child,
|
|
||||||
TimelineCategory category,
|
|
||||||
)? postScreenBuilder;
|
|
||||||
|
|
||||||
final Widget Function(BuildContext context, Widget child)?
|
|
||||||
postCreationScreenBuilder;
|
|
||||||
|
|
||||||
final Widget Function(BuildContext context, Widget child)?
|
|
||||||
postSelectionScreenBuilder;
|
|
||||||
|
|
||||||
final TimelineService service;
|
final TimelineService service;
|
||||||
|
|
||||||
final TimelineUserService userService;
|
final TimelineUserService userService;
|
||||||
|
|
||||||
final TimelineOptions Function(BuildContext context) optionsBuilder;
|
final TimelineOptions Function(BuildContext context) optionsBuilder;
|
||||||
|
|
||||||
final List<TimelineCategory> Function(BuildContext context) categoriesBuilder;
|
final Function(BuildContext context, String userId)? onUserTap;
|
||||||
|
|
||||||
|
final Function(BuildContext context, Widget child)? openPageBuilder;
|
||||||
|
|
||||||
|
final Function(BuildContext context, TimelinePost post)? onPostTap;
|
||||||
|
|
||||||
|
final Widget Function(BuildContext context, TimelinePost post)? onPostDelete;
|
||||||
|
|
||||||
|
final bool filterEnabled;
|
||||||
|
|
||||||
|
final Widget Function(TimelinePost post)? postWidgetBuilder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
|
|
||||||
mixin TimelineUserStoryRoutes {
|
mixin TimelineUserStoryRoutes {
|
||||||
static const String timelineHome = '/timeline';
|
static const String timelineHome = '/timeline';
|
||||||
static const String timelineCreate = '/timeline-create/:category';
|
|
||||||
static String timelineCreatePath(String category) =>
|
|
||||||
'/timeline-create/$category';
|
|
||||||
static const String timelineSelect = '/timeline-select';
|
|
||||||
static const String timelineView = '/timeline-view/:post';
|
static const String timelineView = '/timeline-view/:post';
|
||||||
static String timelineViewPath(String postId) => '/timeline-view/$postId';
|
static String timelineViewPath(String postId) => '/timeline-view/$postId';
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
name: flutter_timeline
|
name: flutter_timeline
|
||||||
description: Visual elements and interface combined into one package
|
description: Visual elements and interface combined into one package
|
||||||
version: 1.0.0
|
version: 2.0.0
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
|
@ -14,16 +14,18 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
go_router: any
|
go_router: any
|
||||||
|
|
||||||
flutter_timeline_view:
|
flutter_timeline_view:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_timeline
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
path: packages/flutter_timeline_view
|
path: packages/flutter_timeline_view
|
||||||
ref: 1.0.0
|
ref: 2.0.0
|
||||||
|
|
||||||
flutter_timeline_interface:
|
flutter_timeline_interface:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_timeline
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
path: packages/flutter_timeline_interface
|
path: packages/flutter_timeline_interface
|
||||||
ref: 1.0.0
|
ref: 2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
|
@ -9,10 +9,11 @@ import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.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:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
class FirebaseTimelineService extends TimelineService with TimelineUserService {
|
||||||
FirebaseTimelineService({
|
FirebaseTimelineService({
|
||||||
required TimelineUserService userService,
|
required TimelineUserService userService,
|
||||||
FirebaseApp? app,
|
FirebaseApp? app,
|
||||||
|
@ -30,7 +31,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
late TimelineUserService _userService;
|
late TimelineUserService _userService;
|
||||||
late FirebaseTimelineOptions _options;
|
late FirebaseTimelineOptions _options;
|
||||||
|
|
||||||
List<TimelinePost> _posts = [];
|
final Map<String, TimelinePosterUserModel> _users = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||||
|
@ -47,14 +48,14 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
var postRef =
|
var postRef =
|
||||||
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
||||||
await postRef.set(updatedPost.toJson());
|
await postRef.set(updatedPost.toJson());
|
||||||
_posts.add(updatedPost);
|
posts.add(updatedPost);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deletePost(TimelinePost post) async {
|
Future<void> 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);
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
await postRef.delete();
|
await postRef.delete();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -72,7 +73,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
reaction: post.reaction - 1,
|
reaction: post.reaction - 1,
|
||||||
reactions: (post.reactions ?? [])..remove(reaction),
|
reactions: (post.reactions ?? [])..remove(reaction),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -102,7 +103,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var updatedPost = post.copyWith(reactions: updatedReactions);
|
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();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +125,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||||
posts.add(post);
|
posts.add(post);
|
||||||
}
|
}
|
||||||
_posts = posts;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
@ -135,12 +136,12 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
// only take posts that are in our category
|
// only take posts that are in our category
|
||||||
var oldestPost = _posts
|
var oldestPost = posts
|
||||||
.where(
|
.where(
|
||||||
(element) => category == null || element.category == category,
|
(element) => category == null || element.category == category,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
_posts.first,
|
posts.first,
|
||||||
(previousValue, element) =>
|
(previousValue, element) =>
|
||||||
(previousValue.createdAt.isBefore(element.createdAt))
|
(previousValue.createdAt.isBefore(element.createdAt))
|
||||||
? previousValue
|
? previousValue
|
||||||
|
@ -161,16 +162,16 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.get();
|
.get();
|
||||||
// add the new posts to the list
|
// add the new posts to the list
|
||||||
var posts = <TimelinePost>[];
|
var newPosts = <TimelinePost>[];
|
||||||
for (var doc in snapshot.docs) {
|
for (var doc in snapshot.docs) {
|
||||||
var data = doc.data();
|
var data = doc.data();
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
var user = await _userService.getUser(data['creator_id']);
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||||
posts.add(post);
|
newPosts.add(post);
|
||||||
}
|
}
|
||||||
_posts = [..._posts, ...posts];
|
posts = [...posts, ...newPosts];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return newPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -185,7 +186,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
|
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
|
||||||
creator: user,
|
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();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
@ -193,12 +194,12 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
@override
|
@override
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
||||||
// fetch all posts between now and the newest posts we have
|
// fetch all posts between now and the newest posts we have
|
||||||
var newestPostWeHave = _posts
|
var newestPostWeHave = posts
|
||||||
.where(
|
.where(
|
||||||
(element) => category == null || element.category == category,
|
(element) => category == null || element.category == category,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
_posts.first,
|
posts.first,
|
||||||
(previousValue, element) =>
|
(previousValue, element) =>
|
||||||
(previousValue.createdAt.isAfter(element.createdAt))
|
(previousValue.createdAt.isAfter(element.createdAt))
|
||||||
? previousValue
|
? previousValue
|
||||||
|
@ -215,26 +216,26 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
.orderBy('created_at', descending: true)
|
.orderBy('created_at', descending: true)
|
||||||
.endBefore([newestPostWeHave.createdAt]).get();
|
.endBefore([newestPostWeHave.createdAt]).get();
|
||||||
// add the new posts to the list
|
// add the new posts to the list
|
||||||
var posts = <TimelinePost>[];
|
var newPosts = <TimelinePost>[];
|
||||||
for (var doc in snapshot.docs) {
|
for (var doc in snapshot.docs) {
|
||||||
var data = doc.data();
|
var data = doc.data();
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
var user = await _userService.getUser(data['creator_id']);
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||||
posts.add(post);
|
newPosts.add(post);
|
||||||
}
|
}
|
||||||
_posts = [...posts, ..._posts];
|
posts = [...posts, ...newPosts];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return newPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TimelinePost? getPost(String postId) =>
|
TimelinePost? getPost(String postId) =>
|
||||||
(_posts.any((element) => element.id == postId))
|
(posts.any((element) => element.id == postId))
|
||||||
? _posts.firstWhere((element) => element.id == postId)
|
? posts.firstWhere((element) => element.id == postId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> getPosts(String? category) => _posts
|
List<TimelinePost> getPosts(String? category) => posts
|
||||||
.where((element) => category == null || element.category == category)
|
.where((element) => category == null || element.category == category)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
@ -245,7 +246,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
likes: post.likes + 1,
|
likes: post.likes + 1,
|
||||||
likedBy: post.likedBy?..add(userId),
|
likedBy: post.likedBy?..add(userId),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -266,7 +267,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
likes: post.likes - 1,
|
likes: post.likes - 1,
|
||||||
likedBy: post.likedBy?..remove(userId),
|
likedBy: post.likedBy?..remove(userId),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -309,7 +310,7 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
'reaction': FieldValue.increment(1),
|
'reaction': FieldValue.increment(1),
|
||||||
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
||||||
});
|
});
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -317,4 +318,34 @@ class FirebaseTimelineService with ChangeNotifier implements TimelineService {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CollectionReference<FirebaseUserDocument> get _userCollection => _db
|
||||||
|
.collection(_options.usersCollectionName)
|
||||||
|
.withConverter<FirebaseUserDocument>(
|
||||||
|
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
|
||||||
|
snapshot.data()!,
|
||||||
|
snapshot.id,
|
||||||
|
),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
Future<TimelinePosterUserModel?> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_timeline_firebase
|
name: flutter_timeline_firebase
|
||||||
description: Implementation of the Flutter Timeline interface for Firebase.
|
description: Implementation of the Flutter Timeline interface for Firebase.
|
||||||
version: 1.0.0
|
version: 2.0.0
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_timeline
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
path: packages/flutter_timeline_interface
|
path: packages/flutter_timeline_interface
|
||||||
ref: 1.0.0
|
ref: 2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
|
@ -8,5 +8,6 @@ export 'src/model/timeline_category.dart';
|
||||||
export 'src/model/timeline_post.dart';
|
export 'src/model/timeline_post.dart';
|
||||||
export 'src/model/timeline_poster.dart';
|
export 'src/model/timeline_poster.dart';
|
||||||
export 'src/model/timeline_reaction.dart';
|
export 'src/model/timeline_reaction.dart';
|
||||||
|
export 'src/services/filter_service.dart';
|
||||||
export 'src/services/timeline_service.dart';
|
export 'src/services/timeline_service.dart';
|
||||||
export 'src/services/user_service.dart';
|
export 'src/services/user_service.dart';
|
||||||
|
|
|
@ -3,13 +3,13 @@ import 'package:flutter/material.dart';
|
||||||
@immutable
|
@immutable
|
||||||
class TimelineCategory {
|
class TimelineCategory {
|
||||||
const TimelineCategory({
|
const TimelineCategory({
|
||||||
required this.name,
|
required this.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
this.canCreate = true,
|
this.canCreate = true,
|
||||||
this.canView = true,
|
this.canView = true,
|
||||||
});
|
});
|
||||||
final String name;
|
final String? key;
|
||||||
final String title;
|
final String title;
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
final bool canCreate;
|
final bool canCreate;
|
||||||
|
|
|
@ -15,12 +15,12 @@ class TimelinePost {
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.creatorId,
|
required this.creatorId,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.category,
|
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.likes,
|
required this.likes,
|
||||||
required this.reaction,
|
required this.reaction,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.reactionEnabled,
|
required this.reactionEnabled,
|
||||||
|
this.category,
|
||||||
this.creator,
|
this.creator,
|
||||||
this.likedBy,
|
this.likedBy,
|
||||||
this.reactions,
|
this.reactions,
|
||||||
|
@ -67,7 +67,7 @@ class TimelinePost {
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
/// The category of the post on which can be filtered.
|
/// The category of the post on which can be filtered.
|
||||||
final String category;
|
final String? category;
|
||||||
|
|
||||||
/// The url of the image of the post.
|
/// The url of the image of the post.
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
|
|
|
@ -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<TimelinePost> filterPosts(
|
||||||
|
String filterWord,
|
||||||
|
Map<String, dynamic> options,
|
||||||
|
) {
|
||||||
|
var filteredPosts = posts
|
||||||
|
.where(
|
||||||
|
(post) => post.title.toLowerCase().contains(
|
||||||
|
filterWord.toLowerCase(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return filteredPosts;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
|
||||||
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
||||||
|
|
||||||
abstract class TimelineService with ChangeNotifier {
|
abstract class TimelineService with ChangeNotifier {
|
||||||
|
List<TimelinePost> posts = [];
|
||||||
|
|
||||||
Future<void> deletePost(TimelinePost post);
|
Future<void> deletePost(TimelinePost post);
|
||||||
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
||||||
Future<TimelinePost> createPost(TimelinePost post);
|
Future<TimelinePost> createPost(TimelinePost post);
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_timeline_interface
|
name: flutter_timeline_interface
|
||||||
description: Interface for the service of the Flutter Timeline component
|
description: Interface for the service of the Flutter Timeline component
|
||||||
version: 1.0.0
|
version: 2.0.0
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
|
|
|
@ -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: <Widget>[
|
|
||||||
Text(
|
|
||||||
'You have pushed the button this many times:',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -12,4 +12,6 @@ export 'src/screens/timeline_post_creation_screen.dart';
|
||||||
export 'src/screens/timeline_post_screen.dart';
|
export 'src/screens/timeline_post_screen.dart';
|
||||||
export 'src/screens/timeline_screen.dart';
|
export 'src/screens/timeline_screen.dart';
|
||||||
export 'src/screens/timeline_selection_screen.dart';
|
export 'src/screens/timeline_selection_screen.dart';
|
||||||
|
export 'src/widgets/category_selector.dart';
|
||||||
|
export 'src/widgets/category_selector_button.dart';
|
||||||
export 'src/widgets/timeline_post_widget.dart';
|
export 'src/widgets/timeline_post_widget.dart';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
// SPDX-FileCopyrightText: 2023 Iconica
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
|
@ -8,7 +9,6 @@ import 'package:flutter_timeline_view/src/config/timeline_theme.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@immutable
|
|
||||||
class TimelineOptions {
|
class TimelineOptions {
|
||||||
const TimelineOptions({
|
const TimelineOptions({
|
||||||
this.theme = const TimelineTheme(),
|
this.theme = const TimelineTheme(),
|
||||||
|
@ -18,21 +18,38 @@ class TimelineOptions {
|
||||||
this.timelinePostHeight,
|
this.timelinePostHeight,
|
||||||
this.allowAllDeletion = false,
|
this.allowAllDeletion = false,
|
||||||
this.sortCommentsAscending = true,
|
this.sortCommentsAscending = true,
|
||||||
this.sortPostsAscending = false,
|
this.sortPostsAscending,
|
||||||
this.dateformat,
|
this.doubleTapTolike = false,
|
||||||
|
this.iconsWithValues = false,
|
||||||
|
this.likeAndDislikeIconsForDoubleTap = const (
|
||||||
|
Icon(
|
||||||
|
Icons.favorite_rounded,
|
||||||
|
color: Color(0xFFC3007A),
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
this.itemInfoBuilder,
|
||||||
|
this.dateFormat,
|
||||||
this.timeFormat,
|
this.timeFormat,
|
||||||
this.buttonBuilder,
|
this.buttonBuilder,
|
||||||
this.textInputBuilder,
|
this.textInputBuilder,
|
||||||
|
this.dividerBuilder,
|
||||||
this.userAvatarBuilder,
|
this.userAvatarBuilder,
|
||||||
this.anonymousAvatarBuilder,
|
this.anonymousAvatarBuilder,
|
||||||
this.nameBuilder,
|
this.nameBuilder,
|
||||||
|
this.padding = const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
this.iconSize = 26,
|
||||||
|
this.postWidgetHeight,
|
||||||
|
this.postPadding = const EdgeInsets.all(12.0),
|
||||||
|
this.filterOptions = const FilterOptions(),
|
||||||
|
this.categoriesOptions = const CategoriesOptions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Theming options for the timeline
|
/// Theming options for the timeline
|
||||||
final TimelineTheme theme;
|
final TimelineTheme theme;
|
||||||
|
|
||||||
/// The format to display the post date in
|
/// The format to display the post date in
|
||||||
final DateFormat? dateformat;
|
final DateFormat? dateFormat;
|
||||||
|
|
||||||
/// The format to display the post time in
|
/// The format to display the post time in
|
||||||
final DateFormat? timeFormat;
|
final DateFormat? timeFormat;
|
||||||
|
@ -41,7 +58,7 @@ class TimelineOptions {
|
||||||
final bool sortCommentsAscending;
|
final bool sortCommentsAscending;
|
||||||
|
|
||||||
/// Whether to sort posts ascending or descending
|
/// Whether to sort posts ascending or descending
|
||||||
final bool sortPostsAscending;
|
final bool? sortPostsAscending;
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
/// Allow all posts to be deleted instead of
|
||||||
/// only the posts of the current user
|
/// only the posts of the current user
|
||||||
|
@ -71,6 +88,97 @@ class TimelineOptions {
|
||||||
/// ImagePickerConfig can be used to define the
|
/// ImagePickerConfig can be used to define the
|
||||||
/// size and quality for the uploaded image.
|
/// size and quality for the uploaded image.
|
||||||
final ImagePickerConfig imagePickerConfig;
|
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;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
/// Sets a predefined height for the postWidget.
|
||||||
|
final double? postWidgetHeight;
|
||||||
|
|
||||||
|
/// Padding of each post
|
||||||
|
final EdgeInsets postPadding;
|
||||||
|
|
||||||
|
/// Options for filtering
|
||||||
|
final FilterOptions filterOptions;
|
||||||
|
|
||||||
|
/// Options for using the category selector.
|
||||||
|
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<TimelineCategory> Function(BuildContext context)?
|
||||||
|
categoriesBuilder;
|
||||||
|
|
||||||
|
/// Abilty to override the standard category selector
|
||||||
|
final Widget Function({
|
||||||
|
required String? categoryKey,
|
||||||
|
required String categoryName,
|
||||||
|
required Function onTap,
|
||||||
|
required bool selected,
|
||||||
|
})? categoryButtonBuilder;
|
||||||
|
|
||||||
|
/// Overides the standard horizontal padding of the whole category selector.
|
||||||
|
final double? categorySelectorHorizontalPadding;
|
||||||
|
|
||||||
|
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<List<TimelinePost>> Function(
|
||||||
|
String filterWord,
|
||||||
|
) search,
|
||||||
|
)? searchBarBuilder;
|
||||||
|
|
||||||
|
final void Function({required bool filterEnabled})? onFilterEnabledChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef ButtonBuilder = Widget Function(
|
typedef ButtonBuilder = Widget Function(
|
||||||
|
|
|
@ -28,6 +28,7 @@ class TimelineTranslations {
|
||||||
required this.postAt,
|
required this.postAt,
|
||||||
required this.postLoadingError,
|
required this.postLoadingError,
|
||||||
required this.timelineSelectionDescription,
|
required this.timelineSelectionDescription,
|
||||||
|
required this.searchHint,
|
||||||
});
|
});
|
||||||
|
|
||||||
const TimelineTranslations.empty()
|
const TimelineTranslations.empty()
|
||||||
|
@ -52,7 +53,8 @@ class TimelineTranslations {
|
||||||
writeComment = 'Write your comment here...',
|
writeComment = 'Write your comment here...',
|
||||||
postAt = 'at',
|
postAt = 'at',
|
||||||
postLoadingError = 'Something went wrong while loading the post',
|
postLoadingError = 'Something went wrong while loading the post',
|
||||||
timelineSelectionDescription = 'Choose a category';
|
timelineSelectionDescription = 'Choose a category',
|
||||||
|
searchHint = 'Search...';
|
||||||
|
|
||||||
final String noPosts;
|
final String noPosts;
|
||||||
final String noPostsWithFilter;
|
final String noPostsWithFilter;
|
||||||
|
@ -79,6 +81,8 @@ class TimelineTranslations {
|
||||||
|
|
||||||
final String timelineSelectionDescription;
|
final String timelineSelectionDescription;
|
||||||
|
|
||||||
|
final String searchHint;
|
||||||
|
|
||||||
TimelineTranslations copyWith({
|
TimelineTranslations copyWith({
|
||||||
String? noPosts,
|
String? noPosts,
|
||||||
String? noPostsWithFilter,
|
String? noPostsWithFilter,
|
||||||
|
@ -101,6 +105,7 @@ class TimelineTranslations {
|
||||||
String? firstComment,
|
String? firstComment,
|
||||||
String? postLoadingError,
|
String? postLoadingError,
|
||||||
String? timelineSelectionDescription,
|
String? timelineSelectionDescription,
|
||||||
|
String? searchHint,
|
||||||
}) =>
|
}) =>
|
||||||
TimelineTranslations(
|
TimelineTranslations(
|
||||||
noPosts: noPosts ?? this.noPosts,
|
noPosts: noPosts ?? this.noPosts,
|
||||||
|
@ -127,5 +132,6 @@ class TimelineTranslations {
|
||||||
postLoadingError: postLoadingError ?? this.postLoadingError,
|
postLoadingError: postLoadingError ?? this.postLoadingError,
|
||||||
timelineSelectionDescription:
|
timelineSelectionDescription:
|
||||||
timelineSelectionDescription ?? this.timelineSelectionDescription,
|
timelineSelectionDescription ?? this.timelineSelectionDescription,
|
||||||
|
searchHint: searchHint ?? this.searchHint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,17 +13,16 @@ import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||||
class TimelinePostCreationScreen extends StatefulWidget {
|
class TimelinePostCreationScreen extends StatefulWidget {
|
||||||
const TimelinePostCreationScreen({
|
const TimelinePostCreationScreen({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.postCategory,
|
|
||||||
required this.onPostCreated,
|
required this.onPostCreated,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.options,
|
required this.options,
|
||||||
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
this.postCategory,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
final String postCategory;
|
final String? postCategory;
|
||||||
|
|
||||||
/// called when the post is created
|
/// called when the post is created
|
||||||
final Function(TimelinePost) onPostCreated;
|
final Function(TimelinePost) onPostCreated;
|
||||||
|
@ -34,9 +33,6 @@ class TimelinePostCreationScreen extends StatefulWidget {
|
||||||
/// The options for the timeline
|
/// The options for the timeline
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
|
|
||||||
/// The padding around the screen
|
|
||||||
final EdgeInsets padding;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TimelinePostCreationScreen> createState() =>
|
State<TimelinePostCreationScreen> createState() =>
|
||||||
_TimelinePostCreationScreenState();
|
_TimelinePostCreationScreenState();
|
||||||
|
@ -92,173 +88,176 @@ class _TimelinePostCreationScreenState
|
||||||
|
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: widget.padding,
|
padding: widget.options.padding,
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
widget.options.translations.title,
|
children: [
|
||||||
style: theme.textTheme.displaySmall,
|
Text(
|
||||||
),
|
widget.options.translations.title,
|
||||||
widget.options.textInputBuilder?.call(
|
style: theme.textTheme.displaySmall,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
widget.options.textInputBuilder?.call(
|
||||||
const SizedBox(
|
titleController,
|
||||||
height: 16,
|
null,
|
||||||
),
|
'',
|
||||||
// input field for the content
|
) ??
|
||||||
Text(
|
TextField(
|
||||||
widget.options.translations.uploadImage,
|
controller: titleController,
|
||||||
style: theme.textTheme.displaySmall,
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
widget.options.translations.uploadImageDescription,
|
widget.options.translations.content,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.displaySmall,
|
||||||
),
|
),
|
||||||
// image picker field
|
const SizedBox(height: 4),
|
||||||
const SizedBox(
|
Text(
|
||||||
height: 8,
|
widget.options.translations.contentDescription,
|
||||||
),
|
style: theme.textTheme.bodyMedium,
|
||||||
Stack(
|
),
|
||||||
children: [
|
// input field for the content
|
||||||
GestureDetector(
|
SizedBox(
|
||||||
onTap: () async {
|
height: 100,
|
||||||
// open a dialog to choose between camera and gallery
|
child: TextField(
|
||||||
var result = await showModalBottomSheet<Uint8List?>(
|
controller: contentController,
|
||||||
context: context,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
builder: (context) => Container(
|
expands: true,
|
||||||
padding: const EdgeInsets.all(8.0),
|
maxLines: null,
|
||||||
color: Colors.black,
|
minLines: null,
|
||||||
child: ImagePicker(
|
),
|
||||||
imagePickerConfig: widget.options.imagePickerConfig,
|
),
|
||||||
imagePickerTheme: widget.options.imagePickerTheme,
|
const SizedBox(
|
||||||
),
|
height: 16,
|
||||||
),
|
),
|
||||||
);
|
// input field for the content
|
||||||
if (result != null) {
|
Text(
|
||||||
setState(() {
|
widget.options.translations.uploadImage,
|
||||||
image = result;
|
style: theme.textTheme.displaySmall,
|
||||||
});
|
),
|
||||||
}
|
Text(
|
||||||
checkIfEditingDone();
|
widget.options.translations.uploadImageDescription,
|
||||||
},
|
style: theme.textTheme.bodyMedium,
|
||||||
child: image != null
|
),
|
||||||
? ClipRRect(
|
// image picker field
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
const SizedBox(
|
||||||
child: Image.memory(
|
height: 8,
|
||||||
image!,
|
),
|
||||||
width: double.infinity,
|
Stack(
|
||||||
height: 150.0,
|
children: [
|
||||||
fit: BoxFit.cover,
|
GestureDetector(
|
||||||
// give it a rounded border
|
onTap: () async {
|
||||||
|
// open a dialog to choose between camera and gallery
|
||||||
|
var result = await showModalBottomSheet<Uint8List?>(
|
||||||
|
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),
|
if (result != null) {
|
||||||
color: theme.textTheme.displayMedium?.color ??
|
setState(() {
|
||||||
Colors.white,
|
image = result;
|
||||||
child: const SizedBox(
|
});
|
||||||
width: double.infinity,
|
}
|
||||||
height: 150.0,
|
checkIfEditingDone();
|
||||||
child: Icon(
|
},
|
||||||
Icons.image,
|
child: image != null
|
||||||
size: 32,
|
? 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
// if an image is selected, show a delete button
|
||||||
],
|
if (image != null) ...[
|
||||||
),
|
Positioned(
|
||||||
|
top: 8,
|
||||||
const SizedBox(height: 16),
|
right: 8,
|
||||||
|
child: GestureDetector(
|
||||||
Text(
|
onTap: () {
|
||||||
widget.options.translations.commentsTitle,
|
setState(() {
|
||||||
style: theme.textTheme.displaySmall,
|
image = null;
|
||||||
),
|
});
|
||||||
Text(
|
checkIfEditingDone();
|
||||||
widget.options.translations.allowCommentsDescription,
|
},
|
||||||
style: theme.textTheme.bodyMedium,
|
child: Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
// radio buttons for yes or no
|
color: Colors.black.withOpacity(0.5),
|
||||||
Switch(
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
value: allowComments,
|
),
|
||||||
onChanged: (newValue) {
|
child: const Icon(
|
||||||
setState(() {
|
Icons.delete,
|
||||||
allowComments = newValue;
|
color: Colors.white,
|
||||||
});
|
),
|
||||||
},
|
),
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,18 +12,17 @@ import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.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/config/timeline_options.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.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';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class TimelinePostScreen extends StatefulWidget {
|
class TimelinePostScreen extends StatelessWidget {
|
||||||
const TimelinePostScreen({
|
const TimelinePostScreen({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.userService,
|
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.post,
|
required this.post,
|
||||||
required this.onPostDelete,
|
required this.onPostDelete,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,31 +32,70 @@ class TimelinePostScreen extends StatefulWidget {
|
||||||
/// The timeline service to fetch the post details
|
/// The timeline service to fetch the post details
|
||||||
final TimelineService service;
|
final TimelineService service;
|
||||||
|
|
||||||
/// The user service to fetch the profile picture of the user
|
|
||||||
final TimelineUserService userService;
|
|
||||||
|
|
||||||
/// Options to configure the timeline screens
|
/// Options to configure the timeline screens
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
|
|
||||||
/// The post to show
|
/// The post to show
|
||||||
final TimelinePost post;
|
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
|
/// If this is not null, the user can tap on the user avatar or name
|
||||||
final Function(String userId)? onUserTap;
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
final VoidCallback onPostDelete;
|
final VoidCallback onPostDelete;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: _TimelinePostScreen(
|
||||||
|
userId: userId,
|
||||||
|
service: service,
|
||||||
|
options: options,
|
||||||
|
post: post,
|
||||||
|
onPostDelete: onPostDelete,
|
||||||
|
onUserTap: onUserTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
class _TimelinePostScreen extends StatefulWidget {
|
||||||
|
const _TimelinePostScreen({
|
||||||
|
required this.userId,
|
||||||
|
required this.service,
|
||||||
|
required this.options,
|
||||||
|
required this.post,
|
||||||
|
required this.onPostDelete,
|
||||||
|
this.onUserTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
final TimelineService service;
|
||||||
|
|
||||||
|
final TimelineOptions options;
|
||||||
|
|
||||||
|
final TimelinePost post;
|
||||||
|
|
||||||
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
|
final VoidCallback onPostDelete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TimelinePostScreen> createState() => _TimelinePostScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
||||||
TimelinePost? post;
|
TimelinePost? post;
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
|
late var textInputBuilder = widget.options.textInputBuilder ??
|
||||||
|
(controller, suffixIcon, hintText) => TextField(
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
suffixIcon: suffixIcon,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -90,11 +128,12 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
var dateFormat = widget.options.dateformat ??
|
var dateFormat = widget.options.dateFormat ??
|
||||||
DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode);
|
DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode);
|
||||||
var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm');
|
var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const Center(
|
const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -127,7 +166,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.padding,
|
padding: widget.options.padding,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -182,12 +221,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
if (widget.options.allowAllDeletion ||
|
if (widget.options.allowAllDeletion ||
|
||||||
post.creator?.userId == widget.userId)
|
post.creator?.userId == widget.userId)
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
onSelected: (value) async {
|
onSelected: (value) => widget.onPostDelete(),
|
||||||
if (value == 'delete') {
|
|
||||||
await widget.service.deletePost(post);
|
|
||||||
widget.onPostDelete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (BuildContext context) =>
|
itemBuilder: (BuildContext context) =>
|
||||||
<PopupMenuEntry<String>>[
|
<PopupMenuEntry<String>>[
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
|
@ -223,11 +257,40 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: CachedNetworkImage(
|
child: widget.options.doubleTapTolike
|
||||||
width: double.infinity,
|
? TappableImage(
|
||||||
imageUrl: post.imageUrl!,
|
likeAndDislikeIcon: widget
|
||||||
fit: BoxFit.fitHeight,
|
.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(
|
const SizedBox(
|
||||||
|
@ -246,11 +309,14 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: widget.options.theme.likedIcon ??
|
child: Container(
|
||||||
Icon(
|
color: Colors.transparent,
|
||||||
Icons.thumb_up_rounded,
|
child: widget.options.theme.likedIcon ??
|
||||||
color: widget.options.theme.iconColor,
|
Icon(
|
||||||
),
|
Icons.thumb_up_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
InkWell(
|
InkWell(
|
||||||
|
@ -262,11 +328,15 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: widget.options.theme.likeIcon ??
|
child: Container(
|
||||||
Icon(
|
color: Colors.transparent,
|
||||||
Icons.thumb_up_alt_outlined,
|
child: widget.options.theme.likeIcon ??
|
||||||
color: widget.options.theme.iconColor,
|
Icon(
|
||||||
),
|
Icons.thumb_up_alt_outlined,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
@ -275,6 +345,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.chat_bubble_outline_rounded,
|
Icons.chat_bubble_outline_rounded,
|
||||||
color: widget.options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -481,14 +552,14 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: ReactionBottom(
|
child: ReactionBottom(
|
||||||
messageInputBuilder: widget.options.textInputBuilder!,
|
messageInputBuilder: textInputBuilder,
|
||||||
onPressSelectImage: () async {
|
onPressSelectImage: () async {
|
||||||
// open the image picker
|
// open the image picker
|
||||||
var result = await showModalBottomSheet<Uint8List?>(
|
var result = await showModalBottomSheet<Uint8List?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Container(
|
builder: (context) => Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
color: Colors.black,
|
color: theme.colorScheme.background,
|
||||||
child: ImagePicker(
|
child: ImagePicker(
|
||||||
imagePickerConfig: widget.options.imagePickerConfig,
|
imagePickerConfig: widget.options.imagePickerConfig,
|
||||||
imagePickerTheme: widget.options.imagePickerTheme,
|
imagePickerTheme: widget.options.imagePickerTheme,
|
||||||
|
|
|
@ -6,20 +6,20 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart';
|
|
||||||
|
|
||||||
class TimelineScreen extends StatefulWidget {
|
class TimelineScreen extends StatefulWidget {
|
||||||
const TimelineScreen({
|
const TimelineScreen({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
|
required this.service,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.onPostTap,
|
required this.onPostTap,
|
||||||
required this.service,
|
this.scrollController,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
this.posts,
|
this.posts,
|
||||||
this.controller,
|
this.timelineCategory,
|
||||||
this.timelineCategoryFilter,
|
this.postWidgetBuilder,
|
||||||
this.padding = const EdgeInsets.symmetric(vertical: 12.0),
|
this.filterEnabled = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,10 +33,10 @@ class TimelineScreen extends StatefulWidget {
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
|
|
||||||
/// The controller for the scroll view
|
/// The controller for the scroll view
|
||||||
final ScrollController? controller;
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
/// The string to filter the timeline by category
|
/// 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
|
/// This is used if you want to pass in a list of posts instead
|
||||||
/// of fetching them from the service
|
/// of fetching them from the service
|
||||||
|
@ -48,8 +48,11 @@ class TimelineScreen extends StatefulWidget {
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
/// If this is not null, the user can tap on the user avatar or name
|
||||||
final Function(String userId)? onUserTap;
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
/// The padding between posts in the timeline
|
/// Override the standard postwidget
|
||||||
final EdgeInsets padding;
|
final Widget Function(TimelinePost post)? postWidgetBuilder;
|
||||||
|
|
||||||
|
/// if true the filter textfield is enabled.
|
||||||
|
final bool filterEnabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TimelineScreen> createState() => _TimelineScreenState();
|
State<TimelineScreen> createState() => _TimelineScreenState();
|
||||||
|
@ -57,12 +60,21 @@ class TimelineScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _TimelineScreenState extends State<TimelineScreen> {
|
class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
late ScrollController controller;
|
late ScrollController controller;
|
||||||
|
late var textFieldController = TextEditingController(
|
||||||
|
text: widget.options.filterOptions.initialFilterWord,
|
||||||
|
);
|
||||||
|
late var service = widget.service;
|
||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
|
late var category = widget.timelineCategory;
|
||||||
|
|
||||||
|
late var filterWord = widget.options.filterOptions.initialFilterWord;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = widget.controller ?? ScrollController();
|
controller = widget.scrollController ?? ScrollController();
|
||||||
unawaited(loadPosts());
|
unawaited(loadPosts());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,60 +86,158 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
|
|
||||||
// Build the list of posts
|
// Build the list of posts
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: widget.service,
|
listenable: service,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
var posts = widget.posts ??
|
var posts = widget.posts ?? service.getPosts(category);
|
||||||
widget.service.getPosts(widget.timelineCategoryFilter);
|
|
||||||
|
if (widget.filterEnabled && filterWord != null) {
|
||||||
|
if (service is TimelineFilterService?) {
|
||||||
|
posts =
|
||||||
|
(service as TimelineFilterService).filterPosts(filterWord!, {});
|
||||||
|
} else {
|
||||||
|
debugPrint('Timeline service needs to mixin'
|
||||||
|
' with TimelineFilterService');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
posts = posts
|
posts = posts
|
||||||
.where(
|
.where(
|
||||||
(p) =>
|
(p) => category == null || p.category == category,
|
||||||
widget.timelineCategoryFilter == null ||
|
|
||||||
p.category == widget.timelineCategoryFilter,
|
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// sort posts by date
|
// sort posts by date
|
||||||
posts.sort(
|
if (widget.options.sortPostsAscending != null) {
|
||||||
(a, b) => widget.options.sortPostsAscending
|
posts.sort(
|
||||||
? a.createdAt.compareTo(b.createdAt)
|
(a, b) => widget.options.sortPostsAscending!
|
||||||
: b.createdAt.compareTo(a.createdAt),
|
? a.createdAt.compareTo(b.createdAt)
|
||||||
);
|
: b.createdAt.compareTo(a.createdAt),
|
||||||
return SingleChildScrollView(
|
);
|
||||||
controller: controller,
|
}
|
||||||
child: Column(
|
|
||||||
children: [
|
return Column(
|
||||||
...posts.map(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
(post) => Padding(
|
children: [
|
||||||
padding: widget.padding,
|
SizedBox(
|
||||||
child: TimelinePostWidget(
|
height: widget.options.padding.top,
|
||||||
userId: widget.userId,
|
),
|
||||||
options: widget.options,
|
if (widget.filterEnabled) ...[
|
||||||
post: post,
|
Padding(
|
||||||
height: widget.options.timelinePostHeight,
|
padding: EdgeInsets.symmetric(
|
||||||
onTap: () => widget.onPostTap.call(post),
|
horizontal: widget.options.padding.horizontal,
|
||||||
onTapLike: () async =>
|
),
|
||||||
widget.service.likePost(widget.userId, post),
|
child: Row(
|
||||||
onTapUnlike: () async =>
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
widget.service.unlikePost(widget.userId, post),
|
children: [
|
||||||
onPostDelete: () async => widget.service.deletePost(post),
|
Expanded(
|
||||||
onUserTap: widget.onUserTap,
|
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();
|
||||||
|
filterWord = null;
|
||||||
|
widget.options.filterOptions.onFilterEnabledChange
|
||||||
|
?.call(filterEnabled: false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Color(0xFF000000),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (posts.isEmpty)
|
const SizedBox(
|
||||||
Center(
|
height: 24,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
CategorySelector(
|
||||||
|
filter: category,
|
||||||
|
options: widget.options,
|
||||||
|
onTapCategory: (categoryKey) {
|
||||||
|
setState(() {
|
||||||
|
category = categoryKey;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: controller,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
...posts.map(
|
||||||
|
(post) => Padding(
|
||||||
|
padding: widget.options.postPadding,
|
||||||
|
child: widget.postWidgetBuilder?.call(post) ??
|
||||||
|
TimelinePostWidget(
|
||||||
|
service: widget.service,
|
||||||
|
userId: widget.userId,
|
||||||
|
options: widget.options,
|
||||||
|
post: post,
|
||||||
|
onTap: () => widget.onPostTap(post),
|
||||||
|
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(
|
||||||
|
category == null
|
||||||
|
? widget.options.translations.noPosts
|
||||||
|
: widget.options.translations.noPostsWithFilter,
|
||||||
|
style: widget.options.theme.textStyles.noPostsStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: widget.options.padding.bottom,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -136,7 +246,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
Future<void> loadPosts() async {
|
Future<void> loadPosts() async {
|
||||||
if (widget.posts != null) return;
|
if (widget.posts != null) return;
|
||||||
try {
|
try {
|
||||||
await widget.service.fetchPosts(widget.timelineCategoryFilter);
|
await service.fetchPosts(category);
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_timeline_view/flutter_timeline_view.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.categoriesOptions.categoriesBuilder == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories = options.categoriesOptions.categoriesBuilder!(context);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width:
|
||||||
|
options.categoriesOptions.categorySelectorHorizontalPadding ??
|
||||||
|
max(options.padding.horizontal - 4, 0),
|
||||||
|
),
|
||||||
|
for (var category in categories) ...[
|
||||||
|
options.categoriesOptions.categoryButtonBuilder?.call(
|
||||||
|
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.key,
|
||||||
|
onTap: () => onTapCategory(category.key),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(
|
||||||
|
width:
|
||||||
|
options.categoriesOptions.categorySelectorHorizontalPadding ??
|
||||||
|
max(options.padding.horizontal - 4, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
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,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelineCategory category;
|
||||||
|
final bool selected;
|
||||||
|
final void Function() onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
style: ButtonStyle(
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
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(
|
||||||
|
category.title,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: selected
|
||||||
|
? theme.colorScheme.onPrimary
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<bool> Function({required bool liked}) onLike;
|
||||||
|
final (Icon?, Icon?) likeAndDislikeIcon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TappableImage> createState() => _TappableImageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TappableImageState extends State<TappableImage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController animationController;
|
||||||
|
late Animation<double> 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<HeartAnimation> createState() => _HeartAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeartAnimationState extends State<HeartAnimation> {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,17 +6,18 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.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({
|
const TimelinePostWidget({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.post,
|
required this.post,
|
||||||
required this.height,
|
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.onTapLike,
|
required this.onTapLike,
|
||||||
required this.onTapUnlike,
|
required this.onTapUnlike,
|
||||||
required this.onPostDelete,
|
required this.onPostDelete,
|
||||||
|
required this.service,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -28,49 +29,57 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
final TimelinePost post;
|
final TimelinePost post;
|
||||||
|
|
||||||
/// Optional max height of the post
|
/// Optional max height of the post
|
||||||
final double? height;
|
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback onTapLike;
|
final VoidCallback onTapLike;
|
||||||
final VoidCallback onTapUnlike;
|
final VoidCallback onTapUnlike;
|
||||||
final VoidCallback onPostDelete;
|
final VoidCallback onPostDelete;
|
||||||
|
final TimelineService service;
|
||||||
|
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
/// If this is not null, the user can tap on the user avatar or name
|
||||||
final Function(String userId)? onUserTap;
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimelinePostWidget> createState() => _TimelinePostWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: widget.onTap,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: post.imageUrl != null ? height : null,
|
height: widget.post.imageUrl != null
|
||||||
|
? widget.options.postWidgetHeight
|
||||||
|
: null,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (post.creator != null)
|
if (widget.post.creator != null)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: onUserTap != null
|
onTap: widget.onUserTap != null
|
||||||
? () => onUserTap?.call(post.creator!.userId)
|
? () =>
|
||||||
|
widget.onUserTap?.call(widget.post.creator!.userId)
|
||||||
: null,
|
: null,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (post.creator!.imageUrl != null) ...[
|
if (widget.post.creator!.imageUrl != null) ...[
|
||||||
options.userAvatarBuilder?.call(
|
widget.options.userAvatarBuilder?.call(
|
||||||
post.creator!,
|
widget.post.creator!,
|
||||||
40,
|
40,
|
||||||
) ??
|
) ??
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
post.creator!.imageUrl!,
|
widget.post.creator!.imageUrl!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
options.anonymousAvatarBuilder?.call(
|
widget.options.anonymousAvatarBuilder?.call(
|
||||||
post.creator!,
|
widget.post.creator!,
|
||||||
40,
|
40,
|
||||||
) ??
|
) ??
|
||||||
const CircleAvatar(
|
const CircleAvatar(
|
||||||
|
@ -82,22 +91,24 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
],
|
],
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
options.nameBuilder?.call(post.creator) ??
|
widget.options.nameBuilder
|
||||||
post.creator?.fullName ??
|
?.call(widget.post.creator) ??
|
||||||
options.translations.anonymousUser,
|
widget.post.creator?.fullName ??
|
||||||
style:
|
widget.options.translations.anonymousUser,
|
||||||
options.theme.textStyles.postCreatorTitleStyle ??
|
style: widget.options.theme.textStyles
|
||||||
theme.textTheme.titleMedium,
|
.postCreatorTitleStyle ??
|
||||||
|
theme.textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (options.allowAllDeletion || post.creator?.userId == userId)
|
if (widget.options.allowAllDeletion ||
|
||||||
|
widget.post.creator?.userId == widget.userId)
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'delete') {
|
if (value == 'delete') {
|
||||||
onPostDelete();
|
widget.onPostDelete();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemBuilder: (BuildContext context) =>
|
itemBuilder: (BuildContext context) =>
|
||||||
|
@ -107,40 +118,67 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
options.translations.deletePost,
|
widget.options.translations.deletePost,
|
||||||
style: options.theme.textStyles.deletePostStyle ??
|
style: widget.options.theme.textStyles
|
||||||
|
.deletePostStyle ??
|
||||||
theme.textTheme.bodyMedium,
|
theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
options.theme.deleteIcon ??
|
widget.options.theme.deleteIcon ??
|
||||||
Icon(
|
Icon(
|
||||||
Icons.delete,
|
Icons.delete,
|
||||||
color: options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: options.theme.moreIcon ??
|
child: widget.options.theme.moreIcon ??
|
||||||
Icon(
|
Icon(
|
||||||
Icons.more_horiz_rounded,
|
Icons.more_horiz_rounded,
|
||||||
color: options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// image of the post
|
// image of the post
|
||||||
if (post.imageUrl != null) ...[
|
if (widget.post.imageUrl != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: height != null ? 1 : 0,
|
flex: widget.options.postWidgetHeight != null ? 1 : 0,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: CachedNetworkImage(
|
child: widget.options.doubleTapTolike
|
||||||
width: double.infinity,
|
? TappableImage(
|
||||||
imageUrl: post.imageUrl!,
|
likeAndDislikeIcon:
|
||||||
fit: BoxFit.fitWidth,
|
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,137 @@ class TimelinePostWidget extends StatelessWidget {
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
// post information
|
// post information
|
||||||
Row(
|
if (widget.options.iconsWithValues)
|
||||||
children: [
|
Row(
|
||||||
if (post.likedBy?.contains(userId) ?? false) ...[
|
children: [
|
||||||
InkWell(
|
TextButton.icon(
|
||||||
onTap: onTapUnlike,
|
onPressed: () async {
|
||||||
child: options.theme.likedIcon ??
|
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(
|
Icon(
|
||||||
Icons.thumb_up_rounded,
|
widget.post.likedBy?.contains(widget.userId) ?? false
|
||||||
color: options.theme.iconColor,
|
? Icons.favorite
|
||||||
),
|
: Icons.favorite_outline_outlined,
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
InkWell(
|
|
||||||
onTap: onTapLike,
|
|
||||||
child: options.theme.likeIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.thumb_up_alt_outlined,
|
|
||||||
color: options.theme.iconColor,
|
|
||||||
),
|
),
|
||||||
|
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)
|
else
|
||||||
options.theme.commentIcon ??
|
Row(
|
||||||
Icon(
|
children: [
|
||||||
Icons.chat_bubble_outline_rounded,
|
if (widget.post.likedBy?.contains(widget.userId) ??
|
||||||
color: options.theme.iconColor,
|
false) ...[
|
||||||
|
InkWell(
|
||||||
|
onTap: widget.onTapUnlike,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: widget.options.theme.likedIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.thumb_up_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
] else ...[
|
||||||
|
InkWell(
|
||||||
|
onTap: widget.onTapLike,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: widget.options.theme.likedIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.thumb_up_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (widget.post.reactionEnabled) ...[
|
||||||
|
Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: widget.options.theme.commentIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.chat_bubble_outline_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
|
|
||||||
Text(
|
if (widget.options.itemInfoBuilder != null) ...[
|
||||||
'${post.likes} ${options.translations.likesTitle}',
|
widget.options.itemInfoBuilder!(
|
||||||
style: options.theme.textStyles.listPostLikeTitleAndAmount ??
|
post: widget.post,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
] else ...[
|
||||||
const SizedBox(height: 4),
|
Text(
|
||||||
Text(
|
'${widget.post.likes} '
|
||||||
options.translations.viewPost,
|
'${widget.options.translations.likesTitle}',
|
||||||
style: options.theme.textStyles.viewPostStyle ??
|
style: widget
|
||||||
theme.textTheme.bodySmall,
|
.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!(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_timeline_view
|
name: flutter_timeline_view
|
||||||
description: Visual elements of the Flutter Timeline Component
|
description: Visual elements of the Flutter Timeline Component
|
||||||
version: 1.0.0
|
version: 2.0.0
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
|
@ -23,11 +23,12 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_timeline
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
path: packages/flutter_timeline_interface
|
path: packages/flutter_timeline_interface
|
||||||
ref: 1.0.0
|
ref: 2.0.0
|
||||||
flutter_image_picker:
|
flutter_image_picker:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||||
ref: 1.0.4
|
ref: 1.0.4
|
||||||
|
collection: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
Loading…
Reference in a new issue