mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-18 18:13:46 +02:00
Merge pull request #54 from Iconica-Development/4.0.0
Improve flutter_timeline for safino usage
This commit is contained in:
commit
9d476129fd
26 changed files with 672 additions and 368 deletions
4
.github/workflows/melos-component-ci.yml
vendored
4
.github/workflows/melos-component-ci.yml
vendored
|
@ -9,4 +9,6 @@ jobs:
|
|||
call-global-iconica-workflow:
|
||||
uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master
|
||||
secrets: inherit
|
||||
permissions: write-all
|
||||
permissions: write-all
|
||||
with:
|
||||
flutter_version: 3.19.6
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,3 +1,28 @@
|
|||
## 4.0.0
|
||||
|
||||
- Add a serviceBuilder to the userstory configuration
|
||||
- Add a listHeaderBuilder for showing a header at the top of the list of posts in the timeline
|
||||
- Add a getUserId function to retrieve the userId when needed in the userstory configuration
|
||||
- Fix the timelinecategory selection by removing the categories with key null
|
||||
- Set an optional max length on the default post title input field
|
||||
- Add a postCreationFloatingActionButtonColor to the timeline theme to set the color of the floating action button
|
||||
- Add a post and a category to the postViewOpenPageBuilder function
|
||||
- Add a refresh functionality to the timeline with a pull to refresh callback to allow additional functionality when refreshing the timeline
|
||||
- Use the adaptive variants of the material elements in the timeline
|
||||
- Change the default blue color to the primary color of the Theme.of(context) in the timeline
|
||||
- Change the TimelineTranslations constructor to require all translations or use the TimelineTranslations.empty constructor if you don't want to specify all translations
|
||||
- Add a TimelinePaddingOptions class to store the padding options for the timeline
|
||||
- fix the avatar size to match the new design
|
||||
- Add the iconbutton for image uploading back to the ReactionBottom
|
||||
- Fix category key is correctly used for saving timeline posts and category title is shown everywhere
|
||||
- Fix when clicking on post delete in the post screen of the userstory it will now navigate back to the timeline and delete the post
|
||||
- Fix like icon being used for both like and unliked posts
|
||||
- Fix post creator can only like the post once and after it is actually created
|
||||
- Change the CategorySelectorButton to use more styling options and allow for an icon to be shown
|
||||
- Fix incorrect timeline reaction name
|
||||
- Add a dialog for post deletion confirmation
|
||||
- Add a callback method to determine if a user can delete posts that gets called when needed
|
||||
|
||||
## 3.0.1
|
||||
|
||||
- Fixed postOverviewScreen not displaying the creators name.
|
||||
|
|
|
@ -7,13 +7,15 @@ TimelineUserStoryConfiguration getConfig(TimelineService service) {
|
|||
userId: 'test_user',
|
||||
optionsBuilder: (context) => options,
|
||||
enablePostOverviewScreen: false,
|
||||
canDeleteAllPosts: (_) => true,
|
||||
);
|
||||
}
|
||||
|
||||
var options = TimelineOptions(
|
||||
textInputBuilder: null,
|
||||
padding: const EdgeInsets.all(20).copyWith(top: 28),
|
||||
allowAllDeletion: true,
|
||||
paddings: TimelinePaddingOptions(
|
||||
mainPadding: const EdgeInsets.all(20).copyWith(top: 28),
|
||||
),
|
||||
categoriesOptions: CategoriesOptions(
|
||||
categoriesBuilder: (context) => [
|
||||
const TimelineCategory(
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||
import 'package:flutter_timeline/src/go_router.dart';
|
||||
|
@ -28,10 +29,13 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelineHome,
|
||||
pageBuilder: (context, state) {
|
||||
var service = config.serviceBuilder?.call(context) ?? config.service;
|
||||
var timelineScreen = TimelineScreen(
|
||||
userId: config.userId,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
service: config.service,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
onRefresh: config.onRefresh,
|
||||
service: service,
|
||||
options: config.optionsBuilder(context),
|
||||
onPostTap: (post) async =>
|
||||
config.onPostTap?.call(context, post) ??
|
||||
|
@ -43,7 +47,11 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
);
|
||||
|
||||
var button = FloatingActionButton(
|
||||
backgroundColor: const Color(0xff71C6D1),
|
||||
backgroundColor: config
|
||||
.optionsBuilder(context)
|
||||
.theme
|
||||
.postCreationFloatingActionButtonColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
onPressed: () async => context.push(
|
||||
TimelineUserStoryRoutes.timelineCategorySelection,
|
||||
),
|
||||
|
@ -67,9 +75,9 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
config
|
||||
.optionsBuilder(context)
|
||||
.translations
|
||||
.timeLineScreenTitle!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
.timeLineScreenTitle,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -87,12 +95,14 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
var timelineSelectionScreen = TimelineSelectionScreen(
|
||||
options: config.optionsBuilder(context),
|
||||
categories: config
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder!(context),
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context) ??
|
||||
[],
|
||||
onCategorySelected: (category) async {
|
||||
await context.push(
|
||||
TimelineUserStoryRoutes.timelinepostCreation(category.title),
|
||||
TimelineUserStoryRoutes.timelinepostCreation(category.key ?? ''),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -113,9 +123,9 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -129,15 +139,30 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelineView,
|
||||
pageBuilder: (context, state) {
|
||||
var post =
|
||||
config.service.postService.getPost(state.pathParameters['post']!);
|
||||
var service = config.serviceBuilder?.call(context) ?? config.service;
|
||||
var post = service.postService.getPost(state.pathParameters['post']!);
|
||||
var category = config.optionsBuilder
|
||||
.call(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context)
|
||||
.firstWhereOrNull(
|
||||
(element) => element.key == post?.category,
|
||||
);
|
||||
|
||||
var timelinePostWidget = TimelinePostScreen(
|
||||
userId: config.userId,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
service: service,
|
||||
post: post!,
|
||||
onPostDelete: () => config.onPostDelete?.call(context, post),
|
||||
onPostDelete: () async =>
|
||||
config.onPostDelete?.call(context, post) ??
|
||||
() async {
|
||||
await service.postService.deletePost(post);
|
||||
if (!context.mounted) return;
|
||||
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||
}.call(),
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
);
|
||||
|
||||
|
@ -150,16 +175,21 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: config.postViewOpenPageBuilder
|
||||
?.call(context, timelinePostWidget, backButton) ??
|
||||
child: config.postViewOpenPageBuilder?.call(
|
||||
context,
|
||||
timelinePostWidget,
|
||||
backButton,
|
||||
post,
|
||||
category,
|
||||
) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
post.category ?? 'Category',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
category?.title ?? post.category ?? 'Category',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -174,19 +204,19 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
path: TimelineUserStoryRoutes.timelinePostCreation,
|
||||
pageBuilder: (context, state) {
|
||||
var category = state.pathParameters['category'];
|
||||
var service = config.serviceBuilder?.call(context) ?? config.service;
|
||||
var timelinePostCreationWidget = TimelinePostCreationScreen(
|
||||
userId: config.userId,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
service: service,
|
||||
onPostCreated: (post) async {
|
||||
var newPost = await config.service.postService.createPost(post);
|
||||
if (context.mounted) {
|
||||
if (config.afterPostCreationGoHome) {
|
||||
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||
} else {
|
||||
await context
|
||||
.push(TimelineUserStoryRoutes.timelineViewPath(newPost.id));
|
||||
}
|
||||
var newPost = await service.postService.createPost(post);
|
||||
if (!context.mounted) return;
|
||||
if (config.afterPostCreationGoHome) {
|
||||
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||
} else {
|
||||
await context
|
||||
.push(TimelineUserStoryRoutes.timelineViewPath(newPost.id));
|
||||
}
|
||||
},
|
||||
onPostOverview: (post) async => context.push(
|
||||
|
@ -216,9 +246,9 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
backgroundColor: const Color(0xff212121),
|
||||
leading: backButton,
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -233,16 +263,15 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
path: TimelineUserStoryRoutes.timelinePostOverview,
|
||||
pageBuilder: (context, state) {
|
||||
var post = state.extra! as TimelinePost;
|
||||
|
||||
var service = config.serviceBuilder?.call(context) ?? config.service;
|
||||
var timelinePostOverviewWidget = TimelinePostOverviewScreen(
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
service: service,
|
||||
timelinePost: post,
|
||||
onPostSubmit: (post) async {
|
||||
await config.service.postService.createPost(post);
|
||||
if (context.mounted) {
|
||||
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||
}
|
||||
await service.postService.createPost(post);
|
||||
if (!context.mounted) return;
|
||||
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||
},
|
||||
);
|
||||
var backButton = IconButton(
|
||||
|
@ -265,9 +294,9 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postOverview!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.postOverview,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
|
|
@ -45,7 +45,8 @@ Widget _timelineScreenRoute({
|
|||
);
|
||||
|
||||
var timelineScreen = TimelineScreen(
|
||||
userId: config.userId,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
service: config.service,
|
||||
options: config.optionsBuilder(context),
|
||||
|
@ -60,12 +61,17 @@ Widget _timelineScreenRoute({
|
|||
),
|
||||
),
|
||||
),
|
||||
onRefresh: config.onRefresh,
|
||||
filterEnabled: config.filterEnabled,
|
||||
postWidgetBuilder: config.postWidgetBuilder,
|
||||
);
|
||||
|
||||
var button = FloatingActionButton(
|
||||
backgroundColor: const Color(0xff71C6D1),
|
||||
backgroundColor: config
|
||||
.optionsBuilder(context)
|
||||
.theme
|
||||
.postCreationFloatingActionButtonColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
onPressed: () async => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postCategorySelectionScreen(
|
||||
|
@ -87,9 +93,9 @@ Widget _timelineScreenRoute({
|
|||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.timeLineScreenTitle!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.timeLineScreenTitle,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -121,16 +127,29 @@ Widget _postDetailScreenRoute({
|
|||
);
|
||||
|
||||
var timelinePostScreen = TimelinePostScreen(
|
||||
userId: config.userId,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
post: post,
|
||||
onPostDelete: () async =>
|
||||
config.onPostDelete?.call(context, post) ??
|
||||
await config.service.postService.deletePost(post),
|
||||
() async {
|
||||
await config.service.postService.deletePost(post);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}.call(),
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
);
|
||||
|
||||
var category = config
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context)
|
||||
.firstWhere((element) => element.key == post.category);
|
||||
|
||||
var backButton = IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
|
@ -138,15 +157,15 @@ Widget _postDetailScreenRoute({
|
|||
);
|
||||
|
||||
return config.postViewOpenPageBuilder
|
||||
?.call(context, timelinePostScreen, backButton) ??
|
||||
?.call(context, timelinePostScreen, backButton, post, category) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
post.category ?? 'Category',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
category?.title ?? post.category ?? 'Category',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -176,34 +195,33 @@ Widget _postCreationScreenRoute({
|
|||
);
|
||||
|
||||
var timelinePostCreationScreen = TimelinePostCreationScreen(
|
||||
userId: config.userId,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
onPostCreated: (post) async {
|
||||
var newPost = await config.service.postService.createPost(post);
|
||||
if (context.mounted) {
|
||||
if (config.afterPostCreationGoHome) {
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _timelineScreenRoute(
|
||||
configuration: config,
|
||||
context: context,
|
||||
),
|
||||
if (!context.mounted) return;
|
||||
if (config.afterPostCreationGoHome) {
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _timelineScreenRoute(
|
||||
configuration: config,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postOverviewScreenRoute(
|
||||
configuration: config,
|
||||
context: context,
|
||||
post: newPost,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postOverviewScreenRoute(
|
||||
configuration: config,
|
||||
context: context,
|
||||
post: newPost,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onPostOverview: (post) async => Navigator.of(context).push(
|
||||
|
@ -216,7 +234,7 @@ Widget _postCreationScreenRoute({
|
|||
),
|
||||
),
|
||||
enablePostOverviewScreen: config.enablePostOverviewScreen,
|
||||
postCategory: category.title,
|
||||
postCategory: category.key,
|
||||
);
|
||||
|
||||
var backButton = IconButton(
|
||||
|
@ -234,9 +252,9 @@ Widget _postCreationScreenRoute({
|
|||
backgroundColor: const Color(0xff212121),
|
||||
leading: backButton,
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -282,7 +300,6 @@ Widget _postOverviewScreenRoute({
|
|||
);
|
||||
}
|
||||
},
|
||||
isOverviewScreen: true,
|
||||
);
|
||||
|
||||
var backButton = IconButton(
|
||||
|
@ -302,9 +319,9 @@ Widget _postOverviewScreenRoute({
|
|||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postOverview!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.postOverview,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
@ -330,9 +347,11 @@ Widget _postCategorySelectionScreen({
|
|||
var timelineSelectionScreen = TimelineSelectionScreen(
|
||||
options: config.optionsBuilder(context),
|
||||
categories: config
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder!(context),
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context) ??
|
||||
[],
|
||||
onCategorySelected: (category) async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
@ -361,9 +380,9 @@ Widget _postCategorySelectionScreen({
|
|||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff71C6D1),
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
|
|
|
@ -48,6 +48,9 @@ class TimelineUserStoryConfiguration {
|
|||
const TimelineUserStoryConfiguration({
|
||||
required this.service,
|
||||
required this.optionsBuilder,
|
||||
this.getUserId,
|
||||
this.serviceBuilder,
|
||||
this.canDeleteAllPosts,
|
||||
this.userId = 'test_user',
|
||||
this.homeOpenPageBuilder,
|
||||
this.postCreationOpenPageBuilder,
|
||||
|
@ -55,6 +58,7 @@ class TimelineUserStoryConfiguration {
|
|||
this.postOverviewOpenPageBuilder,
|
||||
this.onPostTap,
|
||||
this.onUserTap,
|
||||
this.onRefresh,
|
||||
this.onPostDelete,
|
||||
this.filterEnabled = false,
|
||||
this.postWidgetBuilder,
|
||||
|
@ -66,9 +70,19 @@ class TimelineUserStoryConfiguration {
|
|||
/// The ID of the user associated with this user story configuration.
|
||||
final String userId;
|
||||
|
||||
/// A function to get the userId only when needed and with a context
|
||||
final String Function(BuildContext context)? getUserId;
|
||||
|
||||
/// A function to determine if a user can delete posts that is called
|
||||
/// when needed
|
||||
final bool Function(BuildContext context)? canDeleteAllPosts;
|
||||
|
||||
/// The TimelineService responsible for fetching user story data.
|
||||
final TimelineService service;
|
||||
|
||||
/// A function to get the timeline service only when needed and with a context
|
||||
final TimelineService Function(BuildContext context)? serviceBuilder;
|
||||
|
||||
/// A function that builds TimelineOptions based on the given BuildContext.
|
||||
final TimelineOptions Function(BuildContext context) optionsBuilder;
|
||||
|
||||
|
@ -100,6 +114,8 @@ class TimelineUserStoryConfiguration {
|
|||
BuildContext context,
|
||||
Widget child,
|
||||
IconButton? button,
|
||||
TimelinePost post,
|
||||
TimelineCategory? category,
|
||||
)? postViewOpenPageBuilder;
|
||||
|
||||
/// Open page builder function for the post overview page. This function
|
||||
|
@ -117,6 +133,9 @@ class TimelineUserStoryConfiguration {
|
|||
/// A callback function invoked when the user's profile is tapped.
|
||||
final Function(BuildContext context, String userId)? onUserTap;
|
||||
|
||||
/// A callback function invoked when the timeline is refreshed by pulling down
|
||||
final Function(BuildContext context, String? category)? onRefresh;
|
||||
|
||||
/// A callback function invoked when a post deletion is requested.
|
||||
final Widget Function(BuildContext context, TimelinePost post)? onPostDelete;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
name: flutter_timeline
|
||||
description: Visual elements and interface combined into one package
|
||||
version: 3.0.1
|
||||
version: 4.0.0
|
||||
|
||||
publish_to: none
|
||||
|
||||
|
@ -15,17 +15,19 @@ dependencies:
|
|||
sdk: flutter
|
||||
go_router: any
|
||||
|
||||
collection: any
|
||||
|
||||
flutter_timeline_view:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_view
|
||||
ref: 3.0.1
|
||||
ref: 4.0.0
|
||||
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 3.0.1
|
||||
ref: 4.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
|
|
@ -9,10 +9,8 @@ class FirebaseTimelineOptions {
|
|||
const FirebaseTimelineOptions({
|
||||
this.usersCollectionName = 'users',
|
||||
this.timelineCollectionName = 'timeline',
|
||||
this.allTimelineCategories = const [],
|
||||
});
|
||||
|
||||
final String usersCollectionName;
|
||||
final String timelineCollectionName;
|
||||
final List<String> allTimelineCategories;
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ class FirebaseTimelinePostService
|
|||
// update the post with the new like
|
||||
var updatedPost = post.copyWith(
|
||||
likes: post.likes + 1,
|
||||
likedBy: post.likedBy?..add(userId),
|
||||
likedBy: [...post.likedBy ?? [], userId],
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
name: flutter_timeline_firebase
|
||||
description: Implementation of the Flutter Timeline interface for Firebase.
|
||||
version: 3.0.1
|
||||
version: 4.0.0
|
||||
|
||||
publish_to: none
|
||||
|
||||
|
@ -23,7 +23,7 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 3.0.1
|
||||
ref: 4.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
name: flutter_timeline_interface
|
||||
description: Interface for the service of the Flutter Timeline component
|
||||
version: 3.0.1
|
||||
version: 4.0.0
|
||||
|
||||
publish_to: none
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
library flutter_timeline_view;
|
||||
|
||||
export 'src/config/timeline_options.dart';
|
||||
export 'src/config/timeline_paddings.dart';
|
||||
export 'src/config/timeline_styles.dart';
|
||||
export 'src/config/timeline_theme.dart';
|
||||
export 'src/config/timeline_translations.dart';
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_paddings.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_theme.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
@ -12,11 +13,11 @@ import 'package:intl/intl.dart';
|
|||
class TimelineOptions {
|
||||
const TimelineOptions({
|
||||
this.theme = const TimelineTheme(),
|
||||
this.translations = const TimelineTranslations(),
|
||||
this.translations = const TimelineTranslations.empty(),
|
||||
this.paddings = const TimelinePaddingOptions(),
|
||||
this.imagePickerConfig = const ImagePickerConfig(),
|
||||
this.imagePickerTheme = const ImagePickerTheme(),
|
||||
this.timelinePostHeight,
|
||||
this.allowAllDeletion = false,
|
||||
this.sortCommentsAscending = true,
|
||||
this.sortPostsAscending,
|
||||
this.doubleTapTolike = false,
|
||||
|
@ -37,12 +38,8 @@ class TimelineOptions {
|
|||
this.userAvatarBuilder,
|
||||
this.anonymousAvatarBuilder,
|
||||
this.nameBuilder,
|
||||
this.padding =
|
||||
const EdgeInsets.only(left: 12.0, top: 24.0, right: 12.0, bottom: 12.0),
|
||||
this.iconSize = 26,
|
||||
this.postWidgetHeight,
|
||||
this.postPadding =
|
||||
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
|
||||
this.filterOptions = const FilterOptions(),
|
||||
this.categoriesOptions = const CategoriesOptions(),
|
||||
this.requireImageForPost = false,
|
||||
|
@ -52,6 +49,8 @@ class TimelineOptions {
|
|||
this.maxContentLength,
|
||||
this.categorySelectorButtonBuilder,
|
||||
this.postOverviewButtonBuilder,
|
||||
this.deletionDialogBuilder,
|
||||
this.listHeaderBuilder,
|
||||
this.titleInputDecoration,
|
||||
this.contentInputDecoration,
|
||||
});
|
||||
|
@ -71,15 +70,15 @@ class TimelineOptions {
|
|||
/// Whether to sort posts ascending or descending
|
||||
final bool? sortPostsAscending;
|
||||
|
||||
/// Allow all posts to be deleted instead of
|
||||
/// only the posts of the current user
|
||||
final bool allowAllDeletion;
|
||||
|
||||
/// The height of a post in the timeline
|
||||
final double? timelinePostHeight;
|
||||
|
||||
/// Class that contains all the translations used in the timeline
|
||||
final TimelineTranslations translations;
|
||||
|
||||
/// Class that contains all the paddings used in the timeline
|
||||
final TimelinePaddingOptions paddings;
|
||||
|
||||
final ButtonBuilder? buttonBuilder;
|
||||
|
||||
final TextInputBuilder? textInputBuilder;
|
||||
|
@ -115,18 +114,12 @@ class TimelineOptions {
|
|||
/// 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;
|
||||
|
||||
|
@ -156,6 +149,11 @@ class TimelineOptions {
|
|||
String text,
|
||||
)? categorySelectorButtonBuilder;
|
||||
|
||||
/// This widgetbuilder is placed at the top of the list of posts and can be
|
||||
/// used to add custom elements
|
||||
final Widget Function(BuildContext context, String? category)?
|
||||
listHeaderBuilder;
|
||||
|
||||
/// Builder for the post overview button
|
||||
/// on the timeline post overview screen
|
||||
final Widget Function(
|
||||
|
@ -164,6 +162,11 @@ class TimelineOptions {
|
|||
String text,
|
||||
)? postOverviewButtonBuilder;
|
||||
|
||||
/// Optional builder to override the default alertdialog for post deletion
|
||||
/// It should pop the navigator with true to delete the post and
|
||||
/// false to cancel deletion
|
||||
final WidgetBuilder? deletionDialogBuilder;
|
||||
|
||||
/// inputdecoration for the title textfield
|
||||
final InputDecoration? titleInputDecoration;
|
||||
|
||||
|
@ -221,8 +224,7 @@ class CategoriesOptions {
|
|||
|
||||
/// Abilty to override the standard category selector
|
||||
final Widget Function(
|
||||
String? categoryKey,
|
||||
String categoryName,
|
||||
TimelineCategory category,
|
||||
Function() onTap,
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
bool selected,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// This class contains the paddings used in the timeline options
|
||||
class TimelinePaddingOptions {
|
||||
const TimelinePaddingOptions({
|
||||
this.mainPadding =
|
||||
const EdgeInsets.only(left: 12.0, top: 24.0, right: 12.0, bottom: 12.0),
|
||||
this.postPadding =
|
||||
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
|
||||
this.postOverviewButtonBottomPadding = 30.0,
|
||||
this.categoryButtonTextPadding,
|
||||
});
|
||||
|
||||
/// The padding between posts in the timeline
|
||||
final EdgeInsets mainPadding;
|
||||
|
||||
/// The padding of each post
|
||||
final EdgeInsets postPadding;
|
||||
|
||||
/// The bottom padding of the button on the post overview screen
|
||||
final double postOverviewButtonBottomPadding;
|
||||
|
||||
/// The padding between the icon and the text in the category button
|
||||
final double? categoryButtonTextPadding;
|
||||
}
|
|
@ -15,6 +15,9 @@ class TimelineTheme {
|
|||
this.sendIcon,
|
||||
this.moreIcon,
|
||||
this.deleteIcon,
|
||||
this.categorySelectionButtonBorderColor,
|
||||
this.categorySelectionButtonBackgroundColor,
|
||||
this.postCreationFloatingActionButtonColor,
|
||||
this.textStyles = const TimelineTextStyles(),
|
||||
});
|
||||
|
||||
|
@ -38,5 +41,16 @@ class TimelineTheme {
|
|||
/// The icon for delete action (delete post)
|
||||
final Widget? deleteIcon;
|
||||
|
||||
/// The text style overrides for all the texts in the timeline
|
||||
final TimelineTextStyles textStyles;
|
||||
|
||||
/// The color of the border around the category in the selection screen
|
||||
final Color? categorySelectionButtonBorderColor;
|
||||
|
||||
/// The color of the background of the category selection button in the
|
||||
/// selection screen
|
||||
final Color? categorySelectionButtonBackgroundColor;
|
||||
|
||||
/// The color of the floating action button on the overview screen
|
||||
final Color? postCreationFloatingActionButtonColor;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,54 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
|
||||
/// Class that holds all the translations for the timeline component view and
|
||||
/// the corresponding userstory
|
||||
class TimelineTranslations {
|
||||
/// TimelineTranslations constructor where everything is required use this
|
||||
/// if you want to be sure to have all translations specified
|
||||
/// If you just want the default values use the empty constructor
|
||||
/// and optionally override the values with the copyWith method
|
||||
const TimelineTranslations({
|
||||
required this.anonymousUser,
|
||||
required this.noPosts,
|
||||
required this.noPostsWithFilter,
|
||||
required this.title,
|
||||
required this.titleHintText,
|
||||
required this.content,
|
||||
required this.contentHintText,
|
||||
required this.contentDescription,
|
||||
required this.uploadImage,
|
||||
required this.uploadImageDescription,
|
||||
required this.allowComments,
|
||||
required this.allowCommentsDescription,
|
||||
required this.commentsTitleOnPost,
|
||||
required this.checkPost,
|
||||
required this.postAt,
|
||||
required this.deletePost,
|
||||
required this.deleteReaction,
|
||||
required this.deleteConfirmationMessage,
|
||||
required this.deleteConfirmationTitle,
|
||||
required this.deleteCancelButton,
|
||||
required this.deleteButton,
|
||||
required this.viewPost,
|
||||
required this.likesTitle,
|
||||
required this.commentsTitle,
|
||||
required this.firstComment,
|
||||
required this.writeComment,
|
||||
required this.postLoadingError,
|
||||
required this.timelineSelectionDescription,
|
||||
required this.searchHint,
|
||||
required this.postOverview,
|
||||
required this.postIn,
|
||||
required this.postCreation,
|
||||
required this.yes,
|
||||
required this.no,
|
||||
required this.timeLineScreenTitle,
|
||||
});
|
||||
|
||||
/// Default translations for the timeline component view
|
||||
const TimelineTranslations.empty({
|
||||
this.anonymousUser = 'Anonymous user',
|
||||
this.noPosts = 'No posts yet',
|
||||
this.noPostsWithFilter = 'No posts with this filter',
|
||||
|
@ -23,6 +69,11 @@ class TimelineTranslations {
|
|||
this.commentsTitleOnPost = 'Comments',
|
||||
this.checkPost = 'Check post overview',
|
||||
this.deletePost = 'Delete post',
|
||||
this.deleteConfirmationTitle = 'Delete Post',
|
||||
this.deleteConfirmationMessage =
|
||||
'Are you sure you want to delete this post?',
|
||||
this.deleteButton = 'Delete',
|
||||
this.deleteCancelButton = 'Cancel',
|
||||
this.deleteReaction = 'Delete Reaction',
|
||||
this.viewPost = 'View post',
|
||||
this.likesTitle = 'Likes',
|
||||
|
@ -41,45 +92,51 @@ class TimelineTranslations {
|
|||
this.timeLineScreenTitle = 'iconinstagram',
|
||||
});
|
||||
|
||||
final String? noPosts;
|
||||
final String? noPostsWithFilter;
|
||||
final String? anonymousUser;
|
||||
final String noPosts;
|
||||
final String noPostsWithFilter;
|
||||
final String anonymousUser;
|
||||
|
||||
final String? title;
|
||||
final String? content;
|
||||
final String? contentDescription;
|
||||
final String? uploadImage;
|
||||
final String? uploadImageDescription;
|
||||
final String? allowComments;
|
||||
final String? allowCommentsDescription;
|
||||
final String? checkPost;
|
||||
final String? postAt;
|
||||
final String title;
|
||||
final String content;
|
||||
final String contentDescription;
|
||||
final String uploadImage;
|
||||
final String uploadImageDescription;
|
||||
final String allowComments;
|
||||
final String allowCommentsDescription;
|
||||
final String checkPost;
|
||||
final String postAt;
|
||||
|
||||
final String? titleHintText;
|
||||
final String? contentHintText;
|
||||
final String titleHintText;
|
||||
final String contentHintText;
|
||||
|
||||
final String? deletePost;
|
||||
final String? deleteReaction;
|
||||
final String? viewPost;
|
||||
final String? likesTitle;
|
||||
final String? commentsTitle;
|
||||
final String? commentsTitleOnPost;
|
||||
final String? writeComment;
|
||||
final String? firstComment;
|
||||
final String? postLoadingError;
|
||||
final String deletePost;
|
||||
final String deleteConfirmationTitle;
|
||||
final String deleteConfirmationMessage;
|
||||
final String deleteButton;
|
||||
final String deleteCancelButton;
|
||||
|
||||
final String? timelineSelectionDescription;
|
||||
final String deleteReaction;
|
||||
final String viewPost;
|
||||
final String likesTitle;
|
||||
final String commentsTitle;
|
||||
final String commentsTitleOnPost;
|
||||
final String writeComment;
|
||||
final String firstComment;
|
||||
final String postLoadingError;
|
||||
|
||||
final String? searchHint;
|
||||
final String timelineSelectionDescription;
|
||||
|
||||
final String? postOverview;
|
||||
final String? postIn;
|
||||
final String? postCreation;
|
||||
final String searchHint;
|
||||
|
||||
final String? yes;
|
||||
final String? no;
|
||||
final String? timeLineScreenTitle;
|
||||
final String postOverview;
|
||||
final String postIn;
|
||||
final String postCreation;
|
||||
|
||||
final String yes;
|
||||
final String no;
|
||||
final String timeLineScreenTitle;
|
||||
|
||||
/// Method to override the default values of the translations
|
||||
TimelineTranslations copyWith({
|
||||
String? noPosts,
|
||||
String? noPostsWithFilter,
|
||||
|
@ -95,6 +152,10 @@ class TimelineTranslations {
|
|||
String? checkPost,
|
||||
String? postAt,
|
||||
String? deletePost,
|
||||
String? deleteConfirmationTitle,
|
||||
String? deleteConfirmationMessage,
|
||||
String? deleteButton,
|
||||
String? deleteCancelButton,
|
||||
String? deleteReaction,
|
||||
String? viewPost,
|
||||
String? likesTitle,
|
||||
|
@ -130,6 +191,12 @@ class TimelineTranslations {
|
|||
checkPost: checkPost ?? this.checkPost,
|
||||
postAt: postAt ?? this.postAt,
|
||||
deletePost: deletePost ?? this.deletePost,
|
||||
deleteConfirmationTitle:
|
||||
deleteConfirmationTitle ?? this.deleteConfirmationTitle,
|
||||
deleteConfirmationMessage:
|
||||
deleteConfirmationMessage ?? this.deleteConfirmationMessage,
|
||||
deleteButton: deleteButton ?? this.deleteButton,
|
||||
deleteCancelButton: deleteCancelButton ?? this.deleteCancelButton,
|
||||
deleteReaction: deleteReaction ?? this.deleteReaction,
|
||||
viewPost: viewPost ?? this.viewPost,
|
||||
likesTitle: likesTitle ?? this.likesTitle,
|
||||
|
|
|
@ -104,6 +104,7 @@ class _TimelinePostCreationScreenState
|
|||
category: widget.postCategory,
|
||||
content: contentController.text,
|
||||
likes: 0,
|
||||
likedBy: const [],
|
||||
reaction: 0,
|
||||
createdAt: DateTime.now(),
|
||||
reactionEnabled: allowComments,
|
||||
|
@ -121,14 +122,14 @@ class _TimelinePostCreationScreenState
|
|||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Padding(
|
||||
padding: widget.options.padding,
|
||||
padding: widget.options.paddings.mainPadding,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.title!,
|
||||
widget.options.translations.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
|
@ -140,6 +141,7 @@ class _TimelinePostCreationScreenState
|
|||
'',
|
||||
) ??
|
||||
TextField(
|
||||
maxLength: widget.options.maxTitleLength,
|
||||
controller: titleController,
|
||||
decoration: widget.options.contentInputDecoration ??
|
||||
InputDecoration(
|
||||
|
@ -148,7 +150,7 @@ class _TimelinePostCreationScreenState
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.options.translations.content!,
|
||||
widget.options.translations.content,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
|
@ -156,7 +158,7 @@ class _TimelinePostCreationScreenState
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.options.translations.contentDescription!,
|
||||
widget.options.translations.contentDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// input field for the content
|
||||
|
@ -176,14 +178,14 @@ class _TimelinePostCreationScreenState
|
|||
),
|
||||
// input field for the content
|
||||
Text(
|
||||
widget.options.translations.uploadImage!,
|
||||
widget.options.translations.uploadImage,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.uploadImageDescription!,
|
||||
widget.options.translations.uploadImageDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// image picker field
|
||||
|
@ -270,21 +272,21 @@ class _TimelinePostCreationScreenState
|
|||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
widget.options.translations.commentsTitle!,
|
||||
widget.options.translations.commentsTitle,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.allowCommentsDescription!,
|
||||
widget.options.translations.allowCommentsDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
activeColor: const Color(0xff71C6D1),
|
||||
activeColor: theme.colorScheme.primary,
|
||||
value: allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
|
@ -292,9 +294,9 @@ class _TimelinePostCreationScreenState
|
|||
});
|
||||
},
|
||||
),
|
||||
Text(widget.options.translations.yes!),
|
||||
Text(widget.options.translations.yes),
|
||||
Checkbox(
|
||||
activeColor: const Color(0xff71C6D1),
|
||||
activeColor: theme.colorScheme.primary,
|
||||
value: !allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
|
@ -302,7 +304,7 @@ class _TimelinePostCreationScreenState
|
|||
});
|
||||
},
|
||||
),
|
||||
Text(widget.options.translations.no!),
|
||||
Text(widget.options.translations.no),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 120),
|
||||
|
@ -313,13 +315,14 @@ class _TimelinePostCreationScreenState
|
|||
? widget.options.buttonBuilder!(
|
||||
context,
|
||||
onPostCreated,
|
||||
widget.options.translations.checkPost!,
|
||||
widget.options.translations.checkPost,
|
||||
enabled: editingDone,
|
||||
)
|
||||
: ElevatedButton(
|
||||
style: const ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Color(0xff71C6D1)),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onPressed: editingDone
|
||||
? () async {
|
||||
|
@ -332,8 +335,8 @@ class _TimelinePostCreationScreenState
|
|||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
widget.enablePostOverviewScreen
|
||||
? widget.options.translations.checkPost!
|
||||
: widget.options.translations.postCreation!,
|
||||
? widget.options.translations.checkPost
|
||||
: widget.options.translations.postCreation,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||
|
@ -10,27 +11,33 @@ class TimelinePostOverviewScreen extends StatelessWidget {
|
|||
required this.options,
|
||||
required this.service,
|
||||
required this.onPostSubmit,
|
||||
this.isOverviewScreen,
|
||||
super.key,
|
||||
});
|
||||
final TimelinePost timelinePost;
|
||||
final TimelineOptions options;
|
||||
final TimelineService service;
|
||||
final void Function(TimelinePost) onPostSubmit;
|
||||
final bool? isOverviewScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// the timelinePost.category is a key so we need to get the category object
|
||||
var timelineCategoryName = options.categoriesOptions.categoriesBuilder
|
||||
?.call(context)
|
||||
.firstWhereOrNull((element) => element.key == timelinePost.category)
|
||||
?.title ??
|
||||
timelinePost.category;
|
||||
var buttonText = '${options.translations.postIn} $timelineCategoryName';
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
Expanded(
|
||||
child: TimelinePostScreen(
|
||||
userId: timelinePost.creatorId,
|
||||
options: options,
|
||||
post: timelinePost,
|
||||
onPostDelete: () async {},
|
||||
service: service,
|
||||
isOverviewScreen: isOverviewScreen,
|
||||
isOverviewScreen: true,
|
||||
),
|
||||
),
|
||||
options.postOverviewButtonBuilder?.call(
|
||||
|
@ -38,30 +45,37 @@ class TimelinePostOverviewScreen extends StatelessWidget {
|
|||
() {
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
'${options.translations.postIn} ${timelinePost.category}',
|
||||
buttonText,
|
||||
) ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30.0),
|
||||
child: ElevatedButton(
|
||||
style: const ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Color(0xff71C6D1)),
|
||||
),
|
||||
onPressed: () {
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'${options.translations.postIn} ${timelinePost.category}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
options.buttonBuilder?.call(
|
||||
context,
|
||||
() {
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
buttonText,
|
||||
enabled: true,
|
||||
) ??
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Theme.of(context).primaryColor),
|
||||
),
|
||||
onPressed: () {
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
buttonText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: options.paddings.postOverviewButtonBottomPadding),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,15 +13,17 @@ import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class TimelinePostScreen extends StatelessWidget {
|
||||
class TimelinePostScreen extends StatefulWidget {
|
||||
const TimelinePostScreen({
|
||||
required this.userId,
|
||||
required this.service,
|
||||
required this.options,
|
||||
required this.post,
|
||||
required this.onPostDelete,
|
||||
this.allowAllDeletion = false,
|
||||
this.isOverviewScreen = false,
|
||||
this.onUserTap,
|
||||
super.key,
|
||||
|
@ -30,6 +32,10 @@ class TimelinePostScreen extends StatelessWidget {
|
|||
/// The user id of the current user
|
||||
final String userId;
|
||||
|
||||
/// Allow all posts to be deleted instead of
|
||||
/// only the posts of the current user
|
||||
final bool allowAllDeletion;
|
||||
|
||||
/// The timeline service to fetch the post details
|
||||
final TimelineService service;
|
||||
|
||||
|
@ -47,49 +53,10 @@ class TimelinePostScreen extends StatelessWidget {
|
|||
final bool? isOverviewScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: _TimelinePostScreen(
|
||||
userId: userId,
|
||||
service: service,
|
||||
options: options,
|
||||
post: post,
|
||||
onPostDelete: onPostDelete,
|
||||
onUserTap: onUserTap,
|
||||
isOverviewScreen: isOverviewScreen,
|
||||
),
|
||||
);
|
||||
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
||||
}
|
||||
|
||||
class _TimelinePostScreen extends StatefulWidget {
|
||||
const _TimelinePostScreen({
|
||||
required this.userId,
|
||||
required this.service,
|
||||
required this.options,
|
||||
required this.post,
|
||||
required this.onPostDelete,
|
||||
this.onUserTap,
|
||||
this.isOverviewScreen,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
|
||||
final TimelineService service;
|
||||
|
||||
final TimelineOptions options;
|
||||
|
||||
final TimelinePost post;
|
||||
|
||||
final Function(String userId)? onUserTap;
|
||||
|
||||
final VoidCallback onPostDelete;
|
||||
|
||||
final bool? isOverviewScreen;
|
||||
|
||||
@override
|
||||
State<_TimelinePostScreen> createState() => _TimelinePostScreenState();
|
||||
}
|
||||
|
||||
class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
||||
class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||
TimelinePost? post;
|
||||
bool isLoading = true;
|
||||
|
||||
|
@ -146,13 +113,13 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
|
||||
if (isLoading) {
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
if (this.post == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
widget.options.translations.postLoadingError!,
|
||||
widget.options.translations.postLoadingError,
|
||||
style: widget.options.theme.textStyles.errorTextStyle,
|
||||
),
|
||||
);
|
||||
|
@ -166,7 +133,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
|
||||
return Stack(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
updatePost(
|
||||
await widget.service.postService.fetchPostDetails(
|
||||
|
@ -178,7 +145,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: widget.options.padding,
|
||||
padding: widget.options.paddings.mainPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -198,7 +165,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
radius: 14,
|
||||
backgroundImage:
|
||||
CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
|
@ -210,7 +177,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 20,
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
),
|
||||
|
@ -221,7 +188,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
widget.options.nameBuilder
|
||||
?.call(post.creator) ??
|
||||
post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser!,
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles
|
||||
.postCreatorTitleStyle ??
|
||||
theme.textTheme.titleMedium,
|
||||
|
@ -230,10 +197,19 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (widget.options.allowAllDeletion ||
|
||||
post.creator?.userId == widget.userId)
|
||||
if (!(widget.isOverviewScreen ?? false) &&
|
||||
(widget.allowAllDeletion ||
|
||||
post.creator?.userId == widget.userId))
|
||||
PopupMenuButton(
|
||||
onSelected: (value) => widget.onPostDelete(),
|
||||
onSelected: (value) async {
|
||||
if (value == 'delete') {
|
||||
await showPostDeletionConfirmationDialog(
|
||||
widget.options,
|
||||
context,
|
||||
widget.onPostDelete,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
|
@ -241,7 +217,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.deletePost!,
|
||||
widget.options.translations.deletePost,
|
||||
style: widget.options.theme.textStyles
|
||||
.deletePostStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
|
@ -344,6 +320,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
] else ...[
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
if (widget.isOverviewScreen ?? false) return;
|
||||
updatePost(
|
||||
await widget.service.postService.likePost(
|
||||
widget.userId,
|
||||
|
@ -441,7 +418,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
const SizedBox(height: 20),
|
||||
if (post.reactionEnabled) ...[
|
||||
Text(
|
||||
widget.options.translations.commentsTitleOnPost!,
|
||||
widget.options.translations.commentsTitleOnPost,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
for (var reaction
|
||||
|
@ -450,7 +427,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
GestureDetector(
|
||||
onLongPressStart: (details) async {
|
||||
if (reaction.creatorId == widget.userId ||
|
||||
widget.options.allowAllDeletion) {
|
||||
widget.allowAllDeletion) {
|
||||
var overlay = Overlay.of(context)
|
||||
.context
|
||||
.findRenderObject()! as RenderBox;
|
||||
|
@ -469,7 +446,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Text(
|
||||
widget.options.translations.deleteReaction!,
|
||||
widget.options.translations.deleteReaction,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -495,7 +472,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
radius: 14,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
reaction.creator!.imageUrl!,
|
||||
),
|
||||
|
@ -506,7 +483,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 20,
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
),
|
||||
|
@ -520,10 +497,10 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
children: [
|
||||
Text(
|
||||
widget.options.nameBuilder
|
||||
?.call(post.creator) ??
|
||||
?.call(reaction.creator) ??
|
||||
reaction.creator?.fullName ??
|
||||
widget.options.translations
|
||||
.anonymousUser!,
|
||||
.anonymousUser,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
Padding(
|
||||
|
@ -541,7 +518,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: widget.options.nameBuilder
|
||||
?.call(post.creator) ??
|
||||
?.call(reaction.creator) ??
|
||||
reaction.creator?.fullName ??
|
||||
widget
|
||||
.options.translations.anonymousUser,
|
||||
|
@ -565,7 +542,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
if (post.reactions?.isEmpty ?? true) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.options.translations.firstComment!,
|
||||
widget.options.translations.firstComment,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 120),
|
||||
|
@ -575,7 +552,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (post.reactionEnabled && !widget.isOverviewScreen!)
|
||||
if (post.reactionEnabled && !(widget.isOverviewScreen ?? false))
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ReactionBottom(
|
||||
|
|
|
@ -16,16 +16,22 @@ class TimelineScreen extends StatefulWidget {
|
|||
this.onPostTap,
|
||||
this.scrollController,
|
||||
this.onUserTap,
|
||||
this.onRefresh,
|
||||
this.posts,
|
||||
this.timelineCategory,
|
||||
this.postWidgetBuilder,
|
||||
this.filterEnabled = false,
|
||||
this.allowAllDeletion = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The user id of the current user
|
||||
final String userId;
|
||||
|
||||
/// Allow all posts to be deleted instead of
|
||||
/// only the posts of the current user
|
||||
final bool allowAllDeletion;
|
||||
|
||||
/// The service to use for fetching and manipulating posts
|
||||
final TimelineService? service;
|
||||
|
||||
|
@ -45,6 +51,9 @@ class TimelineScreen extends StatefulWidget {
|
|||
/// Called when a post is tapped
|
||||
final Function(TimelinePost)? onPostTap;
|
||||
|
||||
/// Called when the timeline is refreshed by pulling down
|
||||
final Function(BuildContext context, String? category)? onRefresh;
|
||||
|
||||
/// If this is not null, the user can tap on the user avatar or name
|
||||
final Function(String userId)? onUserTap;
|
||||
|
||||
|
@ -104,7 +113,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading && widget.posts == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
// Build the list of posts
|
||||
|
@ -143,12 +152,13 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: widget.options.padding.top,
|
||||
height: widget.options.paddings.mainPadding.top,
|
||||
),
|
||||
if (widget.filterEnabled) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.options.padding.horizontal,
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.options.paddings.mainPadding.left,
|
||||
right: widget.options.paddings.mainPadding.right,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
@ -218,74 +228,87 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
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: service,
|
||||
userId: widget.userId,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
onTap: () async {
|
||||
if (widget.onPostTap != null) {
|
||||
widget.onPostTap!.call(post);
|
||||
child: RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
await widget.onRefresh?.call(context, category);
|
||||
await loadPosts();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Add a optional custom header to the list of posts
|
||||
widget.options.listHeaderBuilder
|
||||
?.call(context, category) ??
|
||||
const SizedBox.shrink(),
|
||||
...posts.map(
|
||||
(post) => Padding(
|
||||
padding: widget.options.paddings.postPadding,
|
||||
child: widget.postWidgetBuilder?.call(post) ??
|
||||
TimelinePostWidget(
|
||||
service: service,
|
||||
userId: widget.userId,
|
||||
options: widget.options,
|
||||
allowAllDeletion: widget.allowAllDeletion,
|
||||
post: post,
|
||||
onTap: () async {
|
||||
if (widget.onPostTap != null) {
|
||||
widget.onPostTap!.call(post);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
body: TimelinePostScreen(
|
||||
userId: 'test_user',
|
||||
service: service,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
onPostDelete: () {
|
||||
service.postService.deletePost(post);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
body: TimelinePostScreen(
|
||||
userId: 'test_user',
|
||||
service: service,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
onPostDelete: () {
|
||||
service.postService
|
||||
.deletePost(post);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTapLike: () async => service.postService
|
||||
.likePost(widget.userId, post),
|
||||
onTapUnlike: () async => service.postService
|
||||
.unlikePost(widget.userId, post),
|
||||
onPostDelete: () async =>
|
||||
service.postService.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,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTapLike: () async => service.postService
|
||||
.likePost(widget.userId, post),
|
||||
onTapUnlike: () async => service.postService
|
||||
.unlikePost(widget.userId, post),
|
||||
onPostDelete: () async =>
|
||||
service.postService.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,
|
||||
height: widget.options.paddings.mainPadding.bottom,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ class TimelineSelectionScreen extends StatelessWidget {
|
|||
Padding(
|
||||
padding: EdgeInsets.only(top: size.height * 0.05, bottom: 8),
|
||||
child: Text(
|
||||
options.translations.timelineSelectionDescription!,
|
||||
options.translations.timelineSelectionDescription,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
|
@ -38,7 +38,7 @@ class TimelineSelectionScreen extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
for (var category in categories.where(
|
||||
(element) => element.canCreate,
|
||||
(element) => element.canCreate && element.key != null,
|
||||
)) ...[
|
||||
options.categorySelectorButtonBuilder?.call(
|
||||
context,
|
||||
|
@ -55,9 +55,13 @@ class TimelineSelectionScreen extends StatelessWidget {
|
|||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: const Color(0xff71C6D1),
|
||||
color:
|
||||
options.theme.categorySelectionButtonBorderColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
color:
|
||||
options.theme.categorySelectionButtonBackgroundColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
|
|
|
@ -37,12 +37,11 @@ class _CategorySelectorState extends State<CategorySelector> {
|
|||
SizedBox(
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.padding.horizontal - 20, 0),
|
||||
max(widget.options.paddings.mainPadding.left - 20, 0),
|
||||
),
|
||||
for (var category in categories) ...[
|
||||
widget.options.categoriesOptions.categoryButtonBuilder?.call(
|
||||
category.key,
|
||||
category.title,
|
||||
category,
|
||||
() => widget.onTapCategory(category.key),
|
||||
widget.filter == category.key,
|
||||
widget.isOnTop,
|
||||
|
@ -61,7 +60,7 @@ class _CategorySelectorState extends State<CategorySelector> {
|
|||
SizedBox(
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.padding.horizontal - 4, 0),
|
||||
max(widget.options.paddings.mainPadding.right - 4, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -14,7 +14,7 @@ class CategorySelectorButton extends StatelessWidget {
|
|||
|
||||
final TimelineCategory category;
|
||||
final bool selected;
|
||||
final void Function() onTap;
|
||||
final VoidCallback onTap;
|
||||
final TimelineOptions options;
|
||||
final bool isOnTop;
|
||||
|
||||
|
@ -36,15 +36,19 @@ class CategorySelectorButton extends StatelessWidget {
|
|||
),
|
||||
fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
selected ? const Color(0xff71C6D1) : Colors.transparent,
|
||||
selected
|
||||
? theme.colorScheme.primary
|
||||
: options.theme.categorySelectionButtonBackgroundColor ??
|
||||
Colors.transparent,
|
||||
),
|
||||
shape: const MaterialStatePropertyAll(
|
||||
shape: MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Color(0xff71C6D1),
|
||||
color: options.theme.categorySelectionButtonBorderColor ??
|
||||
theme.colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
|
@ -53,38 +57,55 @@ class CategorySelectorButton extends StatelessWidget {
|
|||
child: isOnTop
|
||||
? SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Stack(
|
||||
children: [
|
||||
Text(
|
||||
category.title,
|
||||
style: (options.theme.textStyles.categoryTitleStyle ??
|
||||
theme.textTheme.labelLarge)
|
||||
?.copyWith(
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
category.title,
|
||||
style: (options.theme.textStyles.categoryTitleStyle ??
|
||||
theme.textTheme.labelLarge)
|
||||
?.copyWith(
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
],
|
||||
),
|
||||
Center(child: category.icon),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
category.title,
|
||||
style: (options.theme.textStyles.categoryTitleStyle ??
|
||||
theme.textTheme.labelLarge)
|
||||
?.copyWith(
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(
|
||||
children: [
|
||||
category.icon,
|
||||
SizedBox(
|
||||
width:
|
||||
options.paddings.categoryButtonTextPadding ?? 8,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
category.title,
|
||||
style:
|
||||
(options.theme.textStyles.categoryTitleStyle ??
|
||||
theme.textTheme.labelLarge)
|
||||
?.copyWith(
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -49,6 +49,18 @@ class _ReactionBottomState extends State<ReactionBottom> {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.onPressSelectImage != null) ...[
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_textEditingController.clear();
|
||||
widget.onPressSelectImage?.call();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.image,
|
||||
color: widget.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
var value = _textEditingController.text;
|
||||
|
@ -65,7 +77,7 @@ class _ReactionBottomState extends State<ReactionBottom> {
|
|||
],
|
||||
),
|
||||
),
|
||||
widget.translations.writeComment!,
|
||||
widget.translations.writeComment,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -18,12 +18,18 @@ class TimelinePostWidget extends StatefulWidget {
|
|||
required this.onTapUnlike,
|
||||
required this.onPostDelete,
|
||||
required this.service,
|
||||
required this.allowAllDeletion,
|
||||
this.onUserTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The user id of the current user
|
||||
final String userId;
|
||||
|
||||
/// Allow all posts to be deleted instead of
|
||||
/// only the posts of the current user
|
||||
final bool allowAllDeletion;
|
||||
|
||||
final TimelineOptions options;
|
||||
|
||||
final TimelinePost post;
|
||||
|
@ -72,7 +78,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
radius: 14,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
widget.post.creator!.imageUrl!,
|
||||
),
|
||||
|
@ -80,10 +86,10 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
] else ...[
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
40,
|
||||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 20,
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
),
|
||||
|
@ -94,7 +100,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
widget.options.nameBuilder
|
||||
?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser!,
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles
|
||||
.postCreatorTitleStyle ??
|
||||
theme.textTheme.titleMedium,
|
||||
|
@ -103,12 +109,16 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (widget.options.allowAllDeletion ||
|
||||
if (widget.allowAllDeletion ||
|
||||
widget.post.creator?.userId == widget.userId)
|
||||
PopupMenuButton(
|
||||
onSelected: (value) {
|
||||
onSelected: (value) async {
|
||||
if (value == 'delete') {
|
||||
widget.onPostDelete();
|
||||
await showPostDeletionConfirmationDialog(
|
||||
widget.options,
|
||||
context,
|
||||
widget.onPostDelete,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
|
@ -118,7 +128,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.deletePost!,
|
||||
widget.options.translations.deletePost,
|
||||
style: widget.options.theme.textStyles
|
||||
.deletePostStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
|
@ -257,7 +267,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
onTap: widget.onTapLike,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: widget.options.theme.likedIcon ??
|
||||
child: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
Icons.favorite_outline,
|
||||
color: widget.options.theme.iconColor,
|
||||
|
@ -318,7 +328,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.options.translations.viewPost!,
|
||||
widget.options.translations.viewPost,
|
||||
style: widget.options.theme.textStyles.viewPostStyle ??
|
||||
theme.textTheme.bodySmall,
|
||||
),
|
||||
|
@ -331,3 +341,39 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPostDeletionConfirmationDialog(
|
||||
TimelineOptions options,
|
||||
BuildContext context,
|
||||
Function() onPostDelete,
|
||||
) async {
|
||||
var result = await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
options.deletionDialogBuilder?.call(context) ??
|
||||
AlertDialog(
|
||||
title: Text(options.translations.deleteConfirmationTitle),
|
||||
content: Text(options.translations.deleteConfirmationMessage),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(options.translations.deleteCancelButton),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(
|
||||
options.translations.deleteButton,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
onPostDelete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
name: flutter_timeline_view
|
||||
description: Visual elements of the Flutter Timeline Component
|
||||
version: 3.0.1
|
||||
version: 4.0.0
|
||||
|
||||
publish_to: none
|
||||
|
||||
|
@ -23,7 +23,7 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 3.0.1
|
||||
ref: 4.0.0
|
||||
flutter_image_picker:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||
|
|
Loading…
Reference in a new issue