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

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

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

View file

@ -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(

View file

@ -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,
),

View file

@ -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,
),

View file

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

View file

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

View file

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

View file

@ -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(

View file

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

View file

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

View file

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

View file

@ -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,

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

View file

@ -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,

View file

@ -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,

View file

@ -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),
],
);
}

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/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(

View file

@ -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,
),
],
);

View file

@ -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(

View file

@ -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),
),
],
),

View file

@ -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,
),
),
],
),
),
],

View file

@ -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,
),
),
),

View file

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

View file

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