Merge pull request #54 from Iconica-Development/4.0.0

Improve flutter_timeline for safino usage
This commit is contained in:
Gorter-dev 2024-05-29 16:04:18 +02:00 committed by GitHub
commit 9d476129fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 672 additions and 368 deletions

View file

@ -10,3 +10,5 @@ jobs:
uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master
secrets: inherit secrets: inherit
permissions: write-all permissions: write-all
with:
flutter_version: 3.19.6

View file

@ -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 ## 3.0.1
- Fixed postOverviewScreen not displaying the creators name. - Fixed postOverviewScreen not displaying the creators name.

View file

@ -7,13 +7,15 @@ TimelineUserStoryConfiguration getConfig(TimelineService service) {
userId: 'test_user', userId: 'test_user',
optionsBuilder: (context) => options, optionsBuilder: (context) => options,
enablePostOverviewScreen: false, enablePostOverviewScreen: false,
canDeleteAllPosts: (_) => true,
); );
} }
var options = TimelineOptions( var options = TimelineOptions(
textInputBuilder: null, textInputBuilder: null,
padding: const EdgeInsets.all(20).copyWith(top: 28), paddings: TimelinePaddingOptions(
allowAllDeletion: true, mainPadding: const EdgeInsets.all(20).copyWith(top: 28),
),
categoriesOptions: CategoriesOptions( categoriesOptions: CategoriesOptions(
categoriesBuilder: (context) => [ categoriesBuilder: (context) => [
const TimelineCategory( const TimelineCategory(

View file

@ -2,6 +2,7 @@
// //
// 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_timeline/flutter_timeline.dart'; import 'package:flutter_timeline/flutter_timeline.dart';
import 'package:flutter_timeline/src/go_router.dart'; import 'package:flutter_timeline/src/go_router.dart';
@ -28,10 +29,13 @@ List<GoRoute> getTimelineStoryRoutes({
GoRoute( GoRoute(
path: TimelineUserStoryRoutes.timelineHome, path: TimelineUserStoryRoutes.timelineHome,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var service = config.serviceBuilder?.call(context) ?? config.service;
var timelineScreen = TimelineScreen( var timelineScreen = TimelineScreen(
userId: config.userId, userId: config.getUserId?.call(context) ?? config.userId,
onUserTap: (user) => config.onUserTap?.call(context, user), 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), options: config.optionsBuilder(context),
onPostTap: (post) async => onPostTap: (post) async =>
config.onPostTap?.call(context, post) ?? config.onPostTap?.call(context, post) ??
@ -43,7 +47,11 @@ List<GoRoute> getTimelineStoryRoutes({
); );
var button = FloatingActionButton( var button = FloatingActionButton(
backgroundColor: const Color(0xff71C6D1), backgroundColor: config
.optionsBuilder(context)
.theme
.postCreationFloatingActionButtonColor ??
Theme.of(context).primaryColor,
onPressed: () async => context.push( onPressed: () async => context.push(
TimelineUserStoryRoutes.timelineCategorySelection, TimelineUserStoryRoutes.timelineCategorySelection,
), ),
@ -67,9 +75,9 @@ List<GoRoute> getTimelineStoryRoutes({
config config
.optionsBuilder(context) .optionsBuilder(context)
.translations .translations
.timeLineScreenTitle!, .timeLineScreenTitle,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -89,10 +97,12 @@ List<GoRoute> getTimelineStoryRoutes({
categories: config categories: config
.optionsBuilder(context) .optionsBuilder(context)
.categoriesOptions .categoriesOptions
.categoriesBuilder!(context), .categoriesBuilder
?.call(context) ??
[],
onCategorySelected: (category) async { onCategorySelected: (category) async {
await context.push( await context.push(
TimelineUserStoryRoutes.timelinepostCreation(category.title), TimelineUserStoryRoutes.timelinepostCreation(category.key ?? ''),
); );
}, },
); );
@ -113,9 +123,9 @@ List<GoRoute> getTimelineStoryRoutes({
leading: backButton, leading: backButton,
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
config.optionsBuilder(context).translations.postCreation!, config.optionsBuilder(context).translations.postCreation,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -129,15 +139,30 @@ List<GoRoute> getTimelineStoryRoutes({
GoRoute( GoRoute(
path: TimelineUserStoryRoutes.timelineView, path: TimelineUserStoryRoutes.timelineView,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var post = var service = config.serviceBuilder?.call(context) ?? config.service;
config.service.postService.getPost(state.pathParameters['post']!); 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( var timelinePostWidget = TimelinePostScreen(
userId: config.userId, userId: config.getUserId?.call(context) ?? config.userId,
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
service: config.service, service: service,
post: post!, 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), onUserTap: (user) => config.onUserTap?.call(context, user),
); );
@ -150,16 +175,21 @@ List<GoRoute> getTimelineStoryRoutes({
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,
state: state, state: state,
child: config.postViewOpenPageBuilder child: config.postViewOpenPageBuilder?.call(
?.call(context, timelinePostWidget, backButton) ?? context,
timelinePostWidget,
backButton,
post,
category,
) ??
Scaffold( Scaffold(
appBar: AppBar( appBar: AppBar(
leading: backButton, leading: backButton,
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
post.category ?? 'Category', category?.title ?? post.category ?? 'Category',
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -174,20 +204,20 @@ List<GoRoute> getTimelineStoryRoutes({
path: TimelineUserStoryRoutes.timelinePostCreation, path: TimelineUserStoryRoutes.timelinePostCreation,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var category = state.pathParameters['category']; var category = state.pathParameters['category'];
var service = config.serviceBuilder?.call(context) ?? config.service;
var timelinePostCreationWidget = TimelinePostCreationScreen( var timelinePostCreationWidget = TimelinePostCreationScreen(
userId: config.userId, userId: config.getUserId?.call(context) ?? config.userId,
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
service: config.service, service: service,
onPostCreated: (post) async { onPostCreated: (post) async {
var newPost = await config.service.postService.createPost(post); var newPost = await service.postService.createPost(post);
if (context.mounted) { if (!context.mounted) return;
if (config.afterPostCreationGoHome) { if (config.afterPostCreationGoHome) {
context.go(TimelineUserStoryRoutes.timelineHome); context.go(TimelineUserStoryRoutes.timelineHome);
} else { } else {
await context await context
.push(TimelineUserStoryRoutes.timelineViewPath(newPost.id)); .push(TimelineUserStoryRoutes.timelineViewPath(newPost.id));
} }
}
}, },
onPostOverview: (post) async => context.push( onPostOverview: (post) async => context.push(
TimelineUserStoryRoutes.timelinePostOverview, TimelineUserStoryRoutes.timelinePostOverview,
@ -216,9 +246,9 @@ List<GoRoute> getTimelineStoryRoutes({
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
leading: backButton, leading: backButton,
title: Text( title: Text(
config.optionsBuilder(context).translations.postCreation!, config.optionsBuilder(context).translations.postCreation,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -233,16 +263,15 @@ List<GoRoute> getTimelineStoryRoutes({
path: TimelineUserStoryRoutes.timelinePostOverview, path: TimelineUserStoryRoutes.timelinePostOverview,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var post = state.extra! as TimelinePost; var post = state.extra! as TimelinePost;
var service = config.serviceBuilder?.call(context) ?? config.service;
var timelinePostOverviewWidget = TimelinePostOverviewScreen( var timelinePostOverviewWidget = TimelinePostOverviewScreen(
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
service: config.service, service: service,
timelinePost: post, timelinePost: post,
onPostSubmit: (post) async { onPostSubmit: (post) async {
await config.service.postService.createPost(post); await service.postService.createPost(post);
if (context.mounted) { if (!context.mounted) return;
context.go(TimelineUserStoryRoutes.timelineHome); context.go(TimelineUserStoryRoutes.timelineHome);
}
}, },
); );
var backButton = IconButton( var backButton = IconButton(
@ -265,9 +294,9 @@ List<GoRoute> getTimelineStoryRoutes({
leading: backButton, leading: backButton,
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
config.optionsBuilder(context).translations.postOverview!, config.optionsBuilder(context).translations.postOverview,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),

View file

@ -45,7 +45,8 @@ Widget _timelineScreenRoute({
); );
var timelineScreen = TimelineScreen( 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), onUserTap: (user) => config.onUserTap?.call(context, user),
service: config.service, service: config.service,
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
@ -60,12 +61,17 @@ Widget _timelineScreenRoute({
), ),
), ),
), ),
onRefresh: config.onRefresh,
filterEnabled: config.filterEnabled, filterEnabled: config.filterEnabled,
postWidgetBuilder: config.postWidgetBuilder, postWidgetBuilder: config.postWidgetBuilder,
); );
var button = FloatingActionButton( var button = FloatingActionButton(
backgroundColor: const Color(0xff71C6D1), backgroundColor: config
.optionsBuilder(context)
.theme
.postCreationFloatingActionButtonColor ??
Theme.of(context).primaryColor,
onPressed: () async => Navigator.of(context).push( onPressed: () async => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _postCategorySelectionScreen( builder: (context) => _postCategorySelectionScreen(
@ -87,9 +93,9 @@ Widget _timelineScreenRoute({
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
config.optionsBuilder(context).translations.timeLineScreenTitle!, config.optionsBuilder(context).translations.timeLineScreenTitle,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -121,16 +127,29 @@ Widget _postDetailScreenRoute({
); );
var timelinePostScreen = TimelinePostScreen( var timelinePostScreen = TimelinePostScreen(
userId: config.userId, userId: config.getUserId?.call(context) ?? config.userId,
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
service: config.service, service: config.service,
post: post, post: post,
onPostDelete: () async => onPostDelete: () async =>
config.onPostDelete?.call(context, post) ?? 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), 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( var backButton = IconButton(
color: Colors.white, color: Colors.white,
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
@ -138,15 +157,15 @@ Widget _postDetailScreenRoute({
); );
return config.postViewOpenPageBuilder return config.postViewOpenPageBuilder
?.call(context, timelinePostScreen, backButton) ?? ?.call(context, timelinePostScreen, backButton, post, category) ??
Scaffold( Scaffold(
appBar: AppBar( appBar: AppBar(
leading: backButton, leading: backButton,
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
post.category ?? 'Category', category?.title ?? post.category ?? 'Category',
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -176,12 +195,12 @@ Widget _postCreationScreenRoute({
); );
var timelinePostCreationScreen = TimelinePostCreationScreen( var timelinePostCreationScreen = TimelinePostCreationScreen(
userId: config.userId, userId: config.getUserId?.call(context) ?? config.userId,
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
service: config.service, service: config.service,
onPostCreated: (post) async { onPostCreated: (post) async {
var newPost = await config.service.postService.createPost(post); var newPost = await config.service.postService.createPost(post);
if (context.mounted) { if (!context.mounted) return;
if (config.afterPostCreationGoHome) { if (config.afterPostCreationGoHome) {
await Navigator.pushReplacement( await Navigator.pushReplacement(
context, context,
@ -204,7 +223,6 @@ Widget _postCreationScreenRoute({
), ),
); );
} }
}
}, },
onPostOverview: (post) async => Navigator.of(context).push( onPostOverview: (post) async => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@ -216,7 +234,7 @@ Widget _postCreationScreenRoute({
), ),
), ),
enablePostOverviewScreen: config.enablePostOverviewScreen, enablePostOverviewScreen: config.enablePostOverviewScreen,
postCategory: category.title, postCategory: category.key,
); );
var backButton = IconButton( var backButton = IconButton(
@ -234,9 +252,9 @@ Widget _postCreationScreenRoute({
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
leading: backButton, leading: backButton,
title: Text( title: Text(
config.optionsBuilder(context).translations.postCreation!, config.optionsBuilder(context).translations.postCreation,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -282,7 +300,6 @@ Widget _postOverviewScreenRoute({
); );
} }
}, },
isOverviewScreen: true,
); );
var backButton = IconButton( var backButton = IconButton(
@ -302,9 +319,9 @@ Widget _postOverviewScreenRoute({
leading: backButton, leading: backButton,
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
config.optionsBuilder(context).translations.postOverview!, config.optionsBuilder(context).translations.postOverview,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@ -332,7 +349,9 @@ Widget _postCategorySelectionScreen({
categories: config categories: config
.optionsBuilder(context) .optionsBuilder(context)
.categoriesOptions .categoriesOptions
.categoriesBuilder!(context), .categoriesBuilder
?.call(context) ??
[],
onCategorySelected: (category) async { onCategorySelected: (category) async {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@ -361,9 +380,9 @@ Widget _postCategorySelectionScreen({
leading: backButton, leading: backButton,
backgroundColor: const Color(0xff212121), backgroundColor: const Color(0xff212121),
title: Text( title: Text(
config.optionsBuilder(context).translations.postCreation!, config.optionsBuilder(context).translations.postCreation,
style: const TextStyle( style: TextStyle(
color: Color(0xff71C6D1), color: Theme.of(context).primaryColor,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),

View file

@ -48,6 +48,9 @@ class TimelineUserStoryConfiguration {
const TimelineUserStoryConfiguration({ const TimelineUserStoryConfiguration({
required this.service, required this.service,
required this.optionsBuilder, required this.optionsBuilder,
this.getUserId,
this.serviceBuilder,
this.canDeleteAllPosts,
this.userId = 'test_user', this.userId = 'test_user',
this.homeOpenPageBuilder, this.homeOpenPageBuilder,
this.postCreationOpenPageBuilder, this.postCreationOpenPageBuilder,
@ -55,6 +58,7 @@ class TimelineUserStoryConfiguration {
this.postOverviewOpenPageBuilder, this.postOverviewOpenPageBuilder,
this.onPostTap, this.onPostTap,
this.onUserTap, this.onUserTap,
this.onRefresh,
this.onPostDelete, this.onPostDelete,
this.filterEnabled = false, this.filterEnabled = false,
this.postWidgetBuilder, this.postWidgetBuilder,
@ -66,9 +70,19 @@ class TimelineUserStoryConfiguration {
/// The ID of the user associated with this user story configuration. /// The ID of the user associated with this user story configuration.
final String userId; 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. /// The TimelineService responsible for fetching user story data.
final TimelineService service; 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. /// A function that builds TimelineOptions based on the given BuildContext.
final TimelineOptions Function(BuildContext context) optionsBuilder; final TimelineOptions Function(BuildContext context) optionsBuilder;
@ -100,6 +114,8 @@ class TimelineUserStoryConfiguration {
BuildContext context, BuildContext context,
Widget child, Widget child,
IconButton? button, IconButton? button,
TimelinePost post,
TimelineCategory? category,
)? postViewOpenPageBuilder; )? postViewOpenPageBuilder;
/// Open page builder function for the post overview page. This function /// 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. /// A callback function invoked when the user's profile is tapped.
final Function(BuildContext context, String userId)? onUserTap; 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. /// A callback function invoked when a post deletion is requested.
final Widget Function(BuildContext context, TimelinePost post)? onPostDelete; final Widget Function(BuildContext context, TimelinePost post)? onPostDelete;

View file

@ -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: 3.0.1 version: 4.0.0
publish_to: none publish_to: none
@ -15,17 +15,19 @@ dependencies:
sdk: flutter sdk: flutter
go_router: any go_router: any
collection: 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: 3.0.1 ref: 4.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: 3.0.1 ref: 4.0.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0

View file

@ -9,10 +9,8 @@ class FirebaseTimelineOptions {
const FirebaseTimelineOptions({ const FirebaseTimelineOptions({
this.usersCollectionName = 'users', this.usersCollectionName = 'users',
this.timelineCollectionName = 'timeline', this.timelineCollectionName = 'timeline',
this.allTimelineCategories = const [],
}); });
final String usersCollectionName; final String usersCollectionName;
final String timelineCollectionName; final String timelineCollectionName;
final List<String> allTimelineCategories;
} }

View file

@ -254,7 +254,7 @@ class FirebaseTimelinePostService
// update the post with the new like // update the post with the new like
var updatedPost = post.copyWith( var updatedPost = post.copyWith(
likes: post.likes + 1, likes: post.likes + 1,
likedBy: post.likedBy?..add(userId), likedBy: [...post.likedBy ?? [], userId],
); );
posts = posts posts = posts
.map( .map(

View file

@ -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: 3.0.1 version: 4.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: 3.0.1 ref: 4.0.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0

View file

@ -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: 3.0.1 version: 4.0.0
publish_to: none publish_to: none

View file

@ -5,6 +5,7 @@
library flutter_timeline_view; library flutter_timeline_view;
export 'src/config/timeline_options.dart'; export 'src/config/timeline_options.dart';
export 'src/config/timeline_paddings.dart';
export 'src/config/timeline_styles.dart'; export 'src/config/timeline_styles.dart';
export 'src/config/timeline_theme.dart'; export 'src/config/timeline_theme.dart';
export 'src/config/timeline_translations.dart'; export 'src/config/timeline_translations.dart';

View file

@ -5,6 +5,7 @@ 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';
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_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';
@ -12,11 +13,11 @@ import 'package:intl/intl.dart';
class TimelineOptions { class TimelineOptions {
const TimelineOptions({ const TimelineOptions({
this.theme = const TimelineTheme(), this.theme = const TimelineTheme(),
this.translations = const TimelineTranslations(), this.translations = const TimelineTranslations.empty(),
this.paddings = const TimelinePaddingOptions(),
this.imagePickerConfig = const ImagePickerConfig(), this.imagePickerConfig = const ImagePickerConfig(),
this.imagePickerTheme = const ImagePickerTheme(), this.imagePickerTheme = const ImagePickerTheme(),
this.timelinePostHeight, this.timelinePostHeight,
this.allowAllDeletion = false,
this.sortCommentsAscending = true, this.sortCommentsAscending = true,
this.sortPostsAscending, this.sortPostsAscending,
this.doubleTapTolike = false, this.doubleTapTolike = false,
@ -37,12 +38,8 @@ class TimelineOptions {
this.userAvatarBuilder, this.userAvatarBuilder,
this.anonymousAvatarBuilder, this.anonymousAvatarBuilder,
this.nameBuilder, this.nameBuilder,
this.padding =
const EdgeInsets.only(left: 12.0, top: 24.0, right: 12.0, bottom: 12.0),
this.iconSize = 26, this.iconSize = 26,
this.postWidgetHeight, this.postWidgetHeight,
this.postPadding =
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
this.filterOptions = const FilterOptions(), this.filterOptions = const FilterOptions(),
this.categoriesOptions = const CategoriesOptions(), this.categoriesOptions = const CategoriesOptions(),
this.requireImageForPost = false, this.requireImageForPost = false,
@ -52,6 +49,8 @@ class TimelineOptions {
this.maxContentLength, this.maxContentLength,
this.categorySelectorButtonBuilder, this.categorySelectorButtonBuilder,
this.postOverviewButtonBuilder, this.postOverviewButtonBuilder,
this.deletionDialogBuilder,
this.listHeaderBuilder,
this.titleInputDecoration, this.titleInputDecoration,
this.contentInputDecoration, this.contentInputDecoration,
}); });
@ -71,15 +70,15 @@ class TimelineOptions {
/// 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
/// only the posts of the current user
final bool allowAllDeletion;
/// The height of a post in the timeline /// The height of a post in the timeline
final double? timelinePostHeight; final double? timelinePostHeight;
/// Class that contains all the translations used in the timeline
final TimelineTranslations translations; final TimelineTranslations translations;
/// Class that contains all the paddings used in the timeline
final TimelinePaddingOptions paddings;
final ButtonBuilder? buttonBuilder; final ButtonBuilder? buttonBuilder;
final TextInputBuilder? textInputBuilder; final TextInputBuilder? textInputBuilder;
@ -115,18 +114,12 @@ class TimelineOptions {
/// The builder for the divider /// The builder for the divider
final Widget Function()? dividerBuilder; 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 /// Size of icons like the comment and like icons. Dafualts to 26
final double iconSize; final double iconSize;
/// Sets a predefined height for the postWidget. /// Sets a predefined height for the postWidget.
final double? postWidgetHeight; final double? postWidgetHeight;
/// Padding of each post
final EdgeInsets postPadding;
/// Options for filtering /// Options for filtering
final FilterOptions filterOptions; final FilterOptions filterOptions;
@ -156,6 +149,11 @@ class TimelineOptions {
String text, String text,
)? categorySelectorButtonBuilder; )? 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 /// Builder for the post overview button
/// on the timeline post overview screen /// on the timeline post overview screen
final Widget Function( final Widget Function(
@ -164,6 +162,11 @@ class TimelineOptions {
String text, String text,
)? postOverviewButtonBuilder; )? 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 /// inputdecoration for the title textfield
final InputDecoration? titleInputDecoration; final InputDecoration? titleInputDecoration;
@ -221,8 +224,7 @@ class CategoriesOptions {
/// Abilty to override the standard category selector /// Abilty to override the standard category selector
final Widget Function( final Widget Function(
String? categoryKey, TimelineCategory category,
String categoryName,
Function() onTap, Function() onTap,
// ignore: avoid_positional_boolean_parameters // ignore: avoid_positional_boolean_parameters
bool selected, bool selected,

View file

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

View file

@ -15,6 +15,9 @@ class TimelineTheme {
this.sendIcon, this.sendIcon,
this.moreIcon, this.moreIcon,
this.deleteIcon, this.deleteIcon,
this.categorySelectionButtonBorderColor,
this.categorySelectionButtonBackgroundColor,
this.postCreationFloatingActionButtonColor,
this.textStyles = const TimelineTextStyles(), this.textStyles = const TimelineTextStyles(),
}); });
@ -38,5 +41,16 @@ class TimelineTheme {
/// The icon for delete action (delete post) /// The icon for delete action (delete post)
final Widget? deleteIcon; final Widget? deleteIcon;
/// The text style overrides for all the texts in the timeline
final TimelineTextStyles textStyles; 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;
} }

View file

@ -5,8 +5,54 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@immutable @immutable
/// Class that holds all the translations for the timeline component view and
/// the corresponding userstory
class TimelineTranslations { 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({ 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.anonymousUser = 'Anonymous user',
this.noPosts = 'No posts yet', this.noPosts = 'No posts yet',
this.noPostsWithFilter = 'No posts with this filter', this.noPostsWithFilter = 'No posts with this filter',
@ -23,6 +69,11 @@ class TimelineTranslations {
this.commentsTitleOnPost = 'Comments', this.commentsTitleOnPost = 'Comments',
this.checkPost = 'Check post overview', this.checkPost = 'Check post overview',
this.deletePost = 'Delete post', 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.deleteReaction = 'Delete Reaction',
this.viewPost = 'View post', this.viewPost = 'View post',
this.likesTitle = 'Likes', this.likesTitle = 'Likes',
@ -41,45 +92,51 @@ class TimelineTranslations {
this.timeLineScreenTitle = 'iconinstagram', this.timeLineScreenTitle = 'iconinstagram',
}); });
final String? noPosts; final String noPosts;
final String? noPostsWithFilter; final String noPostsWithFilter;
final String? anonymousUser; final String anonymousUser;
final String? title; final String title;
final String? content; final String content;
final String? contentDescription; final String contentDescription;
final String? uploadImage; final String uploadImage;
final String? uploadImageDescription; final String uploadImageDescription;
final String? allowComments; final String allowComments;
final String? allowCommentsDescription; final String allowCommentsDescription;
final String? checkPost; final String checkPost;
final String? postAt; final String postAt;
final String? titleHintText; final String titleHintText;
final String? contentHintText; final String contentHintText;
final String? deletePost; final String deletePost;
final String? deleteReaction; final String deleteConfirmationTitle;
final String? viewPost; final String deleteConfirmationMessage;
final String? likesTitle; final String deleteButton;
final String? commentsTitle; final String deleteCancelButton;
final String? commentsTitleOnPost;
final String? writeComment;
final String? firstComment;
final String? postLoadingError;
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 searchHint;
final String? postIn;
final String? postCreation;
final String? yes; final String postOverview;
final String? no; final String postIn;
final String? timeLineScreenTitle; final String postCreation;
final String yes;
final String no;
final String timeLineScreenTitle;
/// Method to override the default values of the translations
TimelineTranslations copyWith({ TimelineTranslations copyWith({
String? noPosts, String? noPosts,
String? noPostsWithFilter, String? noPostsWithFilter,
@ -95,6 +152,10 @@ class TimelineTranslations {
String? checkPost, String? checkPost,
String? postAt, String? postAt,
String? deletePost, String? deletePost,
String? deleteConfirmationTitle,
String? deleteConfirmationMessage,
String? deleteButton,
String? deleteCancelButton,
String? deleteReaction, String? deleteReaction,
String? viewPost, String? viewPost,
String? likesTitle, String? likesTitle,
@ -130,6 +191,12 @@ class TimelineTranslations {
checkPost: checkPost ?? this.checkPost, checkPost: checkPost ?? this.checkPost,
postAt: postAt ?? this.postAt, postAt: postAt ?? this.postAt,
deletePost: deletePost ?? this.deletePost, deletePost: deletePost ?? this.deletePost,
deleteConfirmationTitle:
deleteConfirmationTitle ?? this.deleteConfirmationTitle,
deleteConfirmationMessage:
deleteConfirmationMessage ?? this.deleteConfirmationMessage,
deleteButton: deleteButton ?? this.deleteButton,
deleteCancelButton: deleteCancelButton ?? this.deleteCancelButton,
deleteReaction: deleteReaction ?? this.deleteReaction, deleteReaction: deleteReaction ?? this.deleteReaction,
viewPost: viewPost ?? this.viewPost, viewPost: viewPost ?? this.viewPost,
likesTitle: likesTitle ?? this.likesTitle, likesTitle: likesTitle ?? this.likesTitle,

View file

@ -104,6 +104,7 @@ class _TimelinePostCreationScreenState
category: widget.postCategory, category: widget.postCategory,
content: contentController.text, content: contentController.text,
likes: 0, likes: 0,
likedBy: const [],
reaction: 0, reaction: 0,
createdAt: DateTime.now(), createdAt: DateTime.now(),
reactionEnabled: allowComments, reactionEnabled: allowComments,
@ -121,14 +122,14 @@ class _TimelinePostCreationScreenState
return GestureDetector( return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
child: Padding( child: Padding(
padding: widget.options.padding, padding: widget.options.paddings.mainPadding,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
widget.options.translations.title!, widget.options.translations.title,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 20, fontSize: 20,
@ -140,6 +141,7 @@ class _TimelinePostCreationScreenState
'', '',
) ?? ) ??
TextField( TextField(
maxLength: widget.options.maxTitleLength,
controller: titleController, controller: titleController,
decoration: widget.options.contentInputDecoration ?? decoration: widget.options.contentInputDecoration ??
InputDecoration( InputDecoration(
@ -148,7 +150,7 @@ class _TimelinePostCreationScreenState
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
widget.options.translations.content!, widget.options.translations.content,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 20, fontSize: 20,
@ -156,7 +158,7 @@ class _TimelinePostCreationScreenState
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
widget.options.translations.contentDescription!, widget.options.translations.contentDescription,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
// input field for the content // input field for the content
@ -176,14 +178,14 @@ class _TimelinePostCreationScreenState
), ),
// input field for the content // input field for the content
Text( Text(
widget.options.translations.uploadImage!, widget.options.translations.uploadImage,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 20, fontSize: 20,
), ),
), ),
Text( Text(
widget.options.translations.uploadImageDescription!, widget.options.translations.uploadImageDescription,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
// image picker field // image picker field
@ -270,21 +272,21 @@ class _TimelinePostCreationScreenState
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
widget.options.translations.commentsTitle!, widget.options.translations.commentsTitle,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 20, fontSize: 20,
), ),
), ),
Text( Text(
widget.options.translations.allowCommentsDescription!, widget.options.translations.allowCommentsDescription,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Checkbox( Checkbox(
activeColor: const Color(0xff71C6D1), activeColor: theme.colorScheme.primary,
value: allowComments, value: allowComments,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
@ -292,9 +294,9 @@ class _TimelinePostCreationScreenState
}); });
}, },
), ),
Text(widget.options.translations.yes!), Text(widget.options.translations.yes),
Checkbox( Checkbox(
activeColor: const Color(0xff71C6D1), activeColor: theme.colorScheme.primary,
value: !allowComments, value: !allowComments,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
@ -302,7 +304,7 @@ class _TimelinePostCreationScreenState
}); });
}, },
), ),
Text(widget.options.translations.no!), Text(widget.options.translations.no),
], ],
), ),
const SizedBox(height: 120), const SizedBox(height: 120),
@ -313,13 +315,14 @@ class _TimelinePostCreationScreenState
? widget.options.buttonBuilder!( ? widget.options.buttonBuilder!(
context, context,
onPostCreated, onPostCreated,
widget.options.translations.checkPost!, widget.options.translations.checkPost,
enabled: editingDone, enabled: editingDone,
) )
: ElevatedButton( : ElevatedButton(
style: const ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor: MaterialStatePropertyAll(
MaterialStatePropertyAll(Color(0xff71C6D1)), theme.colorScheme.primary,
),
), ),
onPressed: editingDone onPressed: editingDone
? () async { ? () async {
@ -332,8 +335,8 @@ class _TimelinePostCreationScreenState
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Text( child: Text(
widget.enablePostOverviewScreen widget.enablePostOverviewScreen
? widget.options.translations.checkPost! ? widget.options.translations.checkPost
: widget.options.translations.postCreation!, : widget.options.translations.postCreation,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 20, fontSize: 20,

View file

@ -1,5 +1,6 @@
// ignore_for_file: prefer_expression_function_bodies // ignore_for_file: prefer_expression_function_bodies
import 'package:collection/collection.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/flutter_timeline_view.dart'; import 'package:flutter_timeline_view/flutter_timeline_view.dart';
@ -10,27 +11,33 @@ class TimelinePostOverviewScreen extends StatelessWidget {
required this.options, required this.options,
required this.service, required this.service,
required this.onPostSubmit, required this.onPostSubmit,
this.isOverviewScreen,
super.key, super.key,
}); });
final TimelinePost timelinePost; final TimelinePost timelinePost;
final TimelineOptions options; final TimelineOptions options;
final TimelineService service; final TimelineService service;
final void Function(TimelinePost) onPostSubmit; final void Function(TimelinePost) onPostSubmit;
final bool? isOverviewScreen;
@override @override
Widget build(BuildContext context) { 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( return Column(
mainAxisSize: MainAxisSize.max,
children: [ children: [
Flexible( Expanded(
child: TimelinePostScreen( child: TimelinePostScreen(
userId: timelinePost.creatorId, userId: timelinePost.creatorId,
options: options, options: options,
post: timelinePost, post: timelinePost,
onPostDelete: () async {}, onPostDelete: () async {},
service: service, service: service,
isOverviewScreen: isOverviewScreen, isOverviewScreen: true,
), ),
), ),
options.postOverviewButtonBuilder?.call( options.postOverviewButtonBuilder?.call(
@ -38,13 +45,20 @@ class TimelinePostOverviewScreen extends StatelessWidget {
() { () {
onPostSubmit(timelinePost); onPostSubmit(timelinePost);
}, },
'${options.translations.postIn} ${timelinePost.category}', buttonText,
) ?? ) ??
Padding( options.buttonBuilder?.call(
padding: const EdgeInsets.only(bottom: 30.0), context,
child: ElevatedButton( () {
style: const ButtonStyle( onPostSubmit(timelinePost);
backgroundColor: MaterialStatePropertyAll(Color(0xff71C6D1)), },
buttonText,
enabled: true,
) ??
ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Theme.of(context).primaryColor),
), ),
onPressed: () { onPressed: () {
onPostSubmit(timelinePost); onPostSubmit(timelinePost);
@ -52,7 +66,7 @@ class TimelinePostOverviewScreen extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Text( child: Text(
'${options.translations.postIn} ${timelinePost.category}', buttonText,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 20, fontSize: 20,
@ -61,7 +75,7 @@ class TimelinePostOverviewScreen extends StatelessWidget {
), ),
), ),
), ),
), SizedBox(height: options.paddings.postOverviewButtonBottomPadding),
], ],
); );
} }

View file

@ -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/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:flutter_timeline_view/src/widgets/tappable_image.dart';
import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class TimelinePostScreen extends StatelessWidget { class TimelinePostScreen extends StatefulWidget {
const TimelinePostScreen({ const TimelinePostScreen({
required this.userId, required this.userId,
required this.service, required this.service,
required this.options, required this.options,
required this.post, required this.post,
required this.onPostDelete, required this.onPostDelete,
this.allowAllDeletion = false,
this.isOverviewScreen = false, this.isOverviewScreen = false,
this.onUserTap, this.onUserTap,
super.key, super.key,
@ -30,6 +32,10 @@ class TimelinePostScreen extends StatelessWidget {
/// The user id of the current user /// The user id of the current user
final String userId; 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 /// The timeline service to fetch the post details
final TimelineService service; final TimelineService service;
@ -47,49 +53,10 @@ class TimelinePostScreen extends StatelessWidget {
final bool? isOverviewScreen; final bool? isOverviewScreen;
@override @override
Widget build(BuildContext context) => Scaffold( State<TimelinePostScreen> createState() => _TimelinePostScreenState();
body: _TimelinePostScreen(
userId: userId,
service: service,
options: options,
post: post,
onPostDelete: onPostDelete,
onUserTap: onUserTap,
isOverviewScreen: isOverviewScreen,
),
);
} }
class _TimelinePostScreen extends StatefulWidget { class _TimelinePostScreenState extends State<TimelinePostScreen> {
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> {
TimelinePost? post; TimelinePost? post;
bool isLoading = true; bool isLoading = true;
@ -146,13 +113,13 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
if (isLoading) { if (isLoading) {
const Center( const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator.adaptive(),
); );
} }
if (this.post == null) { if (this.post == null) {
return Center( return Center(
child: Text( child: Text(
widget.options.translations.postLoadingError!, widget.options.translations.postLoadingError,
style: widget.options.theme.textStyles.errorTextStyle, style: widget.options.theme.textStyles.errorTextStyle,
), ),
); );
@ -166,7 +133,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
return Stack( return Stack(
children: [ children: [
RefreshIndicator( RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
updatePost( updatePost(
await widget.service.postService.fetchPostDetails( await widget.service.postService.fetchPostDetails(
@ -178,7 +145,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: widget.options.padding, padding: widget.options.paddings.mainPadding,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -198,7 +165,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
28, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 20, radius: 14,
backgroundImage: backgroundImage:
CachedNetworkImageProvider( CachedNetworkImageProvider(
post.creator!.imageUrl!, post.creator!.imageUrl!,
@ -210,7 +177,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
28, 28,
) ?? ) ??
const CircleAvatar( const CircleAvatar(
radius: 20, radius: 14,
child: Icon( child: Icon(
Icons.person, Icons.person,
), ),
@ -221,7 +188,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
widget.options.nameBuilder widget.options.nameBuilder
?.call(post.creator) ?? ?.call(post.creator) ??
post.creator?.fullName ?? post.creator?.fullName ??
widget.options.translations.anonymousUser!, widget.options.translations.anonymousUser,
style: widget.options.theme.textStyles style: widget.options.theme.textStyles
.postCreatorTitleStyle ?? .postCreatorTitleStyle ??
theme.textTheme.titleMedium, theme.textTheme.titleMedium,
@ -230,10 +197,19 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
), ),
), ),
const Spacer(), const Spacer(),
if (widget.options.allowAllDeletion || if (!(widget.isOverviewScreen ?? false) &&
post.creator?.userId == widget.userId) (widget.allowAllDeletion ||
post.creator?.userId == widget.userId))
PopupMenuButton( PopupMenuButton(
onSelected: (value) => widget.onPostDelete(), onSelected: (value) async {
if (value == 'delete') {
await showPostDeletionConfirmationDialog(
widget.options,
context,
widget.onPostDelete,
);
}
},
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[ <PopupMenuEntry<String>>[
PopupMenuItem<String>( PopupMenuItem<String>(
@ -241,7 +217,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
child: Row( child: Row(
children: [ children: [
Text( Text(
widget.options.translations.deletePost!, widget.options.translations.deletePost,
style: widget.options.theme.textStyles style: widget.options.theme.textStyles
.deletePostStyle ?? .deletePostStyle ??
theme.textTheme.bodyMedium, theme.textTheme.bodyMedium,
@ -344,6 +320,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
] else ...[ ] else ...[
InkWell( InkWell(
onTap: () async { onTap: () async {
if (widget.isOverviewScreen ?? false) return;
updatePost( updatePost(
await widget.service.postService.likePost( await widget.service.postService.likePost(
widget.userId, widget.userId,
@ -441,7 +418,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
const SizedBox(height: 20), const SizedBox(height: 20),
if (post.reactionEnabled) ...[ if (post.reactionEnabled) ...[
Text( Text(
widget.options.translations.commentsTitleOnPost!, widget.options.translations.commentsTitleOnPost,
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
), ),
for (var reaction for (var reaction
@ -450,7 +427,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
GestureDetector( GestureDetector(
onLongPressStart: (details) async { onLongPressStart: (details) async {
if (reaction.creatorId == widget.userId || if (reaction.creatorId == widget.userId ||
widget.options.allowAllDeletion) { widget.allowAllDeletion) {
var overlay = Overlay.of(context) var overlay = Overlay.of(context)
.context .context
.findRenderObject()! as RenderBox; .findRenderObject()! as RenderBox;
@ -469,7 +446,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
PopupMenuItem<String>( PopupMenuItem<String>(
value: 'delete', value: 'delete',
child: Text( child: Text(
widget.options.translations.deleteReaction!, widget.options.translations.deleteReaction,
), ),
), ),
], ],
@ -495,7 +472,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
28, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 20, radius: 14,
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
reaction.creator!.imageUrl!, reaction.creator!.imageUrl!,
), ),
@ -506,7 +483,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
28, 28,
) ?? ) ??
const CircleAvatar( const CircleAvatar(
radius: 20, radius: 14,
child: Icon( child: Icon(
Icons.person, Icons.person,
), ),
@ -520,10 +497,10 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
children: [ children: [
Text( Text(
widget.options.nameBuilder widget.options.nameBuilder
?.call(post.creator) ?? ?.call(reaction.creator) ??
reaction.creator?.fullName ?? reaction.creator?.fullName ??
widget.options.translations widget.options.translations
.anonymousUser!, .anonymousUser,
style: theme.textTheme.titleSmall, style: theme.textTheme.titleSmall,
), ),
Padding( Padding(
@ -541,7 +518,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
text: widget.options.nameBuilder text: widget.options.nameBuilder
?.call(post.creator) ?? ?.call(reaction.creator) ??
reaction.creator?.fullName ?? reaction.creator?.fullName ??
widget widget
.options.translations.anonymousUser, .options.translations.anonymousUser,
@ -565,7 +542,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
if (post.reactions?.isEmpty ?? true) ...[ if (post.reactions?.isEmpty ?? true) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
widget.options.translations.firstComment!, widget.options.translations.firstComment,
), ),
], ],
const SizedBox(height: 120), 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( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ReactionBottom( child: ReactionBottom(

View file

@ -16,16 +16,22 @@ class TimelineScreen extends StatefulWidget {
this.onPostTap, this.onPostTap,
this.scrollController, this.scrollController,
this.onUserTap, this.onUserTap,
this.onRefresh,
this.posts, this.posts,
this.timelineCategory, this.timelineCategory,
this.postWidgetBuilder, this.postWidgetBuilder,
this.filterEnabled = false, this.filterEnabled = false,
this.allowAllDeletion = false,
super.key, super.key,
}); });
/// The user id of the current user /// The user id of the current user
final String userId; 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 /// The service to use for fetching and manipulating posts
final TimelineService? service; final TimelineService? service;
@ -45,6 +51,9 @@ class TimelineScreen extends StatefulWidget {
/// Called when a post is tapped /// Called when a post is tapped
final Function(TimelinePost)? onPostTap; 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 /// 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;
@ -104,7 +113,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isLoading && widget.posts == null) { if (isLoading && widget.posts == null) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator.adaptive());
} }
// Build the list of posts // Build the list of posts
@ -143,12 +152,13 @@ class _TimelineScreenState extends State<TimelineScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( SizedBox(
height: widget.options.padding.top, height: widget.options.paddings.mainPadding.top,
), ),
if (widget.filterEnabled) ...[ if (widget.filterEnabled) ...[
Padding( Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.only(
horizontal: widget.options.padding.horizontal, left: widget.options.paddings.mainPadding.left,
right: widget.options.paddings.mainPadding.right,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@ -218,19 +228,29 @@ class _TimelineScreenState extends State<TimelineScreen> {
height: 12, height: 12,
), ),
Expanded( Expanded(
child: RefreshIndicator.adaptive(
onRefresh: () async {
await widget.onRefresh?.call(context, category);
await loadPosts();
},
child: SingleChildScrollView( child: SingleChildScrollView(
controller: controller, controller: controller,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
/// Add a optional custom header to the list of posts
widget.options.listHeaderBuilder
?.call(context, category) ??
const SizedBox.shrink(),
...posts.map( ...posts.map(
(post) => Padding( (post) => Padding(
padding: widget.options.postPadding, padding: widget.options.paddings.postPadding,
child: widget.postWidgetBuilder?.call(post) ?? child: widget.postWidgetBuilder?.call(post) ??
TimelinePostWidget( TimelinePostWidget(
service: service, service: service,
userId: widget.userId, userId: widget.userId,
options: widget.options, options: widget.options,
allowAllDeletion: widget.allowAllDeletion,
post: post, post: post,
onTap: () async { onTap: () async {
if (widget.onPostTap != null) { if (widget.onPostTap != null) {
@ -249,7 +269,8 @@ class _TimelineScreenState extends State<TimelineScreen> {
options: widget.options, options: widget.options,
post: post, post: post,
onPostDelete: () { onPostDelete: () {
service.postService.deletePost(post); service.postService
.deletePost(post);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@ -273,10 +294,11 @@ class _TimelineScreenState extends State<TimelineScreen> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
category == null category == null
? widget.options.translations.noPosts! ? widget.options.translations.noPosts
: widget : widget
.options.translations.noPostsWithFilter!, .options.translations.noPostsWithFilter,
style: widget.options.theme.textStyles.noPostsStyle, style:
widget.options.theme.textStyles.noPostsStyle,
), ),
), ),
), ),
@ -284,8 +306,9 @@ class _TimelineScreenState extends State<TimelineScreen> {
), ),
), ),
), ),
),
SizedBox( SizedBox(
height: widget.options.padding.bottom, height: widget.options.paddings.mainPadding.bottom,
), ),
], ],
); );

View file

@ -29,7 +29,7 @@ class TimelineSelectionScreen extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.only(top: size.height * 0.05, bottom: 8), padding: EdgeInsets.only(top: size.height * 0.05, bottom: 8),
child: Text( child: Text(
options.translations.timelineSelectionDescription!, options.translations.timelineSelectionDescription,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
fontSize: 20, fontSize: 20,
@ -38,7 +38,7 @@ class TimelineSelectionScreen extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
for (var category in categories.where( for (var category in categories.where(
(element) => element.canCreate, (element) => element.canCreate && element.key != null,
)) ...[ )) ...[
options.categorySelectorButtonBuilder?.call( options.categorySelectorButtonBuilder?.call(
context, context,
@ -55,9 +55,13 @@ class TimelineSelectionScreen extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all( border: Border.all(
color: const Color(0xff71C6D1), color:
options.theme.categorySelectionButtonBorderColor ??
Theme.of(context).primaryColor,
width: 2, width: 2,
), ),
color:
options.theme.categorySelectionButtonBackgroundColor,
), ),
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
child: Column( child: Column(

View file

@ -37,12 +37,11 @@ class _CategorySelectorState extends State<CategorySelector> {
SizedBox( SizedBox(
width: widget.options.categoriesOptions width: widget.options.categoriesOptions
.categorySelectorHorizontalPadding ?? .categorySelectorHorizontalPadding ??
max(widget.options.padding.horizontal - 20, 0), max(widget.options.paddings.mainPadding.left - 20, 0),
), ),
for (var category in categories) ...[ for (var category in categories) ...[
widget.options.categoriesOptions.categoryButtonBuilder?.call( widget.options.categoriesOptions.categoryButtonBuilder?.call(
category.key, category,
category.title,
() => widget.onTapCategory(category.key), () => widget.onTapCategory(category.key),
widget.filter == category.key, widget.filter == category.key,
widget.isOnTop, widget.isOnTop,
@ -61,7 +60,7 @@ class _CategorySelectorState extends State<CategorySelector> {
SizedBox( SizedBox(
width: widget.options.categoriesOptions width: widget.options.categoriesOptions
.categorySelectorHorizontalPadding ?? .categorySelectorHorizontalPadding ??
max(widget.options.padding.horizontal - 4, 0), max(widget.options.paddings.mainPadding.right - 4, 0),
), ),
], ],
), ),

View file

@ -14,7 +14,7 @@ class CategorySelectorButton extends StatelessWidget {
final TimelineCategory category; final TimelineCategory category;
final bool selected; final bool selected;
final void Function() onTap; final VoidCallback onTap;
final TimelineOptions options; final TimelineOptions options;
final bool isOnTop; final bool isOnTop;
@ -36,15 +36,19 @@ class CategorySelectorButton extends StatelessWidget {
), ),
fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)), fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
backgroundColor: MaterialStatePropertyAll( backgroundColor: MaterialStatePropertyAll(
selected ? const Color(0xff71C6D1) : Colors.transparent, selected
? theme.colorScheme.primary
: options.theme.categorySelectionButtonBackgroundColor ??
Colors.transparent,
), ),
shape: const MaterialStatePropertyAll( shape: MaterialStatePropertyAll(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
side: BorderSide( side: BorderSide(
color: Color(0xff71C6D1), color: options.theme.categorySelectionButtonBorderColor ??
theme.colorScheme.primary,
width: 2, width: 2,
), ),
), ),
@ -53,7 +57,9 @@ class CategorySelectorButton extends StatelessWidget {
child: isOnTop child: isOnTop
? SizedBox( ? SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
child: Column( child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -70,13 +76,25 @@ class CategorySelectorButton extends StatelessWidget {
), ),
], ],
), ),
Center(child: category.icon),
],
),
) )
: Row( : Row(
children: [ children: [
Flexible( Flexible(
child: Row(
children: [
category.icon,
SizedBox(
width:
options.paddings.categoryButtonTextPadding ?? 8,
),
Expanded(
child: Text( child: Text(
category.title, category.title,
style: (options.theme.textStyles.categoryTitleStyle ?? style:
(options.theme.textStyles.categoryTitleStyle ??
theme.textTheme.labelLarge) theme.textTheme.labelLarge)
?.copyWith( ?.copyWith(
color: selected color: selected
@ -90,6 +108,9 @@ class CategorySelectorButton extends StatelessWidget {
], ],
), ),
), ),
],
),
),
); );
} }
} }

View file

@ -49,6 +49,18 @@ class _ReactionBottomState extends State<ReactionBottom> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.onPressSelectImage != null) ...[
IconButton(
onPressed: () async {
_textEditingController.clear();
widget.onPressSelectImage?.call();
},
icon: Icon(
Icons.image,
color: widget.iconColor,
),
),
],
IconButton( IconButton(
onPressed: () async { onPressed: () async {
var value = _textEditingController.text; var value = _textEditingController.text;
@ -65,7 +77,7 @@ class _ReactionBottomState extends State<ReactionBottom> {
], ],
), ),
), ),
widget.translations.writeComment!, widget.translations.writeComment,
), ),
), ),
), ),

View file

@ -18,12 +18,18 @@ class TimelinePostWidget extends StatefulWidget {
required this.onTapUnlike, required this.onTapUnlike,
required this.onPostDelete, required this.onPostDelete,
required this.service, required this.service,
required this.allowAllDeletion,
this.onUserTap, this.onUserTap,
super.key, super.key,
}); });
/// The user id of the current user /// The user id of the current user
final String userId; 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 TimelineOptions options;
final TimelinePost post; final TimelinePost post;
@ -72,7 +78,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
28, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 20, radius: 14,
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
widget.post.creator!.imageUrl!, widget.post.creator!.imageUrl!,
), ),
@ -80,10 +86,10 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
] else ...[ ] else ...[
widget.options.anonymousAvatarBuilder?.call( widget.options.anonymousAvatarBuilder?.call(
widget.post.creator!, widget.post.creator!,
40, 28,
) ?? ) ??
const CircleAvatar( const CircleAvatar(
radius: 20, radius: 14,
child: Icon( child: Icon(
Icons.person, Icons.person,
), ),
@ -94,7 +100,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
widget.options.nameBuilder widget.options.nameBuilder
?.call(widget.post.creator) ?? ?.call(widget.post.creator) ??
widget.post.creator?.fullName ?? widget.post.creator?.fullName ??
widget.options.translations.anonymousUser!, widget.options.translations.anonymousUser,
style: widget.options.theme.textStyles style: widget.options.theme.textStyles
.postCreatorTitleStyle ?? .postCreatorTitleStyle ??
theme.textTheme.titleMedium, theme.textTheme.titleMedium,
@ -103,12 +109,16 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
), ),
), ),
const Spacer(), const Spacer(),
if (widget.options.allowAllDeletion || if (widget.allowAllDeletion ||
widget.post.creator?.userId == widget.userId) widget.post.creator?.userId == widget.userId)
PopupMenuButton( PopupMenuButton(
onSelected: (value) { onSelected: (value) async {
if (value == 'delete') { if (value == 'delete') {
widget.onPostDelete(); await showPostDeletionConfirmationDialog(
widget.options,
context,
widget.onPostDelete,
);
} }
}, },
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
@ -118,7 +128,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
child: Row( child: Row(
children: [ children: [
Text( Text(
widget.options.translations.deletePost!, widget.options.translations.deletePost,
style: widget.options.theme.textStyles style: widget.options.theme.textStyles
.deletePostStyle ?? .deletePostStyle ??
theme.textTheme.bodyMedium, theme.textTheme.bodyMedium,
@ -257,7 +267,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
onTap: widget.onTapLike, onTap: widget.onTapLike,
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
child: widget.options.theme.likedIcon ?? child: widget.options.theme.likeIcon ??
Icon( Icon(
Icons.favorite_outline, Icons.favorite_outline,
color: widget.options.theme.iconColor, color: widget.options.theme.iconColor,
@ -318,7 +328,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
widget.options.translations.viewPost!, widget.options.translations.viewPost,
style: widget.options.theme.textStyles.viewPostStyle ?? style: widget.options.theme.textStyles.viewPostStyle ??
theme.textTheme.bodySmall, 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();
}
}

View file

@ -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: 3.0.1 version: 4.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: 3.0.1 ref: 4.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