mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-18 18:13:46 +02:00
commit
554c4526da
52 changed files with 1771 additions and 1532 deletions
4
.github/workflows/melos-component-ci.yml
vendored
4
.github/workflows/melos-component-ci.yml
vendored
|
@ -9,6 +9,4 @@ jobs:
|
|||
call-global-iconica-workflow:
|
||||
uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master
|
||||
secrets: inherit
|
||||
permissions: write-all
|
||||
with:
|
||||
flutter_version: 3.19.6
|
||||
permissions: write-all
|
|
@ -1,3 +1,8 @@
|
|||
## 5.1.0
|
||||
|
||||
* Added `routeToPostDetail` to the `TimelineUserStory` to allow for navigation to the post detail screen.
|
||||
* Fixed design issues.
|
||||
|
||||
## 4.1.0
|
||||
- Migrate to flutter 3.22 which deprecates the background and onBackground properties in the ThemeData and also removes MaterialStatePropertyAll
|
||||
- Add categorySelectionButtonSelectedTextColor and categorySelectionButtonUnselectedTextColor to the timeline theme to allow for the customization of the text color of the category selection buttons
|
||||
|
|
40
README.md
40
README.md
|
@ -35,45 +35,7 @@ And import this package: import 'package:intl/date_symbol_data_local.dart';
|
|||
```
|
||||
|
||||
## How to use
|
||||
To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
|
||||
|
||||
Add go_router as dependency to your project.
|
||||
Add the following configuration to your flutter_application:
|
||||
|
||||
```
|
||||
List<GoRoute> getTimelineStoryRoutes() =>
|
||||
getTimelineStoryRoutes(
|
||||
TimelineUserStoryConfiguration(
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) {
|
||||
return const TimelineOptions();
|
||||
},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Add the `getTimelineStoryRoutes()` to your go_router routes like so:
|
||||
|
||||
```
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const MyHomePage(
|
||||
title: "home",
|
||||
);
|
||||
},
|
||||
),
|
||||
...getTimelineStoryRoutes(configuration: configuration);
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
The user story can also be used without go router:
|
||||
Add the following code somewhere in your widget tree:
|
||||
To use the userstory add the following code somewhere in your widget tree:
|
||||
|
||||
````
|
||||
timeLineNavigatorUserStory(TimelineUserStoryConfiguration, context),
|
||||
|
|
1
packages/flutter_timeline/CHANGELOG.md
Symbolic link
1
packages/flutter_timeline/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/flutter_timeline/LICENSE
Symbolic link
1
packages/flutter_timeline/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
1
packages/flutter_timeline/README.md
Symbolic link
1
packages/flutter_timeline/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../README.md
|
|
@ -1,41 +0,0 @@
|
|||
import 'package:example/config/config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
List<GoRoute> getTimelineRoutes() => getTimelineStoryRoutes(
|
||||
configuration: getConfig(TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
)),
|
||||
);
|
||||
|
||||
final _router = GoRouter(
|
||||
initialLocation: '/timeline',
|
||||
routes: [
|
||||
...getTimelineRoutes(),
|
||||
],
|
||||
);
|
||||
|
||||
class GoRouterApp extends StatelessWidget {
|
||||
const GoRouterApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: 'Flutter Timeline',
|
||||
theme: ThemeData(
|
||||
textTheme: const TextTheme(
|
||||
titleLarge: TextStyle(
|
||||
color: Color(0xffb71c6d), fontFamily: 'Playfair Display')),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFB8E2E8),
|
||||
primary: const Color(0xffb71c6d),
|
||||
).copyWith(
|
||||
surface: const Color(0XFFFAF9F6),
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,25 +16,6 @@ var options = TimelineOptions(
|
|||
paddings: TimelinePaddingOptions(
|
||||
mainPadding: const EdgeInsets.all(20).copyWith(top: 28),
|
||||
),
|
||||
categoriesOptions: CategoriesOptions(
|
||||
categoriesBuilder: (context) => [
|
||||
const TimelineCategory(
|
||||
key: null,
|
||||
title: 'All',
|
||||
icon: SizedBox.shrink(),
|
||||
),
|
||||
const TimelineCategory(
|
||||
key: 'category1',
|
||||
title: 'Category 1',
|
||||
icon: SizedBox.shrink(),
|
||||
),
|
||||
const TimelineCategory(
|
||||
key: 'category2',
|
||||
title: 'Category 2',
|
||||
icon: SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
void navigateToOverview(
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
// import 'package:example/apps/go_router/app.dart';
|
||||
// import 'package:example/apps/navigator/app.dart';
|
||||
import 'package:example/apps/go_router/app.dart';
|
||||
import 'package:example/apps/navigator/app.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
||||
void main() {
|
||||
initializeDateFormatting();
|
||||
|
||||
// Uncomment any, but only one, of these lines to run the example with specific navigation.
|
||||
|
||||
// runApp(const WidgetApp());
|
||||
// runApp(const NavigatorApp());
|
||||
runApp(const GoRouterApp());
|
||||
runApp(const NavigatorApp());
|
||||
}
|
||||
|
|
|
@ -38,7 +38,6 @@ dependencies:
|
|||
flutter_timeline:
|
||||
path: ../
|
||||
intl: ^0.19.0
|
||||
go_router: ^13.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
/// Flutter Timeline library
|
||||
library flutter_timeline;
|
||||
|
||||
export 'package:flutter_timeline/src/flutter_timeline_gorouter_userstory.dart';
|
||||
export 'package:flutter_timeline/src/flutter_timeline_navigator_userstory.dart';
|
||||
export 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
||||
export 'package:flutter_timeline/src/routes.dart';
|
||||
|
|
|
@ -1,311 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Iconica
|
||||
//
|
||||
// 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';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Retrieves a list of GoRouter routes for timeline stories.
|
||||
///
|
||||
/// This function retrieves a list of GoRouter routes for displaying timeline
|
||||
/// stories. It takes an optional [TimelineUserStoryConfiguration] as parameter.
|
||||
/// If no configuration is provided, default values will be used.
|
||||
List<GoRoute> getTimelineStoryRoutes({
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
return <GoRoute>[
|
||||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelineHome,
|
||||
pageBuilder: (context, state) {
|
||||
var service = config.serviceBuilder?.call(context) ?? config.service;
|
||||
var timelineScreen = TimelineScreen(
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
onRefresh: config.onRefresh,
|
||||
service: service,
|
||||
options: config.optionsBuilder(context),
|
||||
onPostTap: (post) async =>
|
||||
config.onPostTap?.call(context, post) ??
|
||||
await context.push(
|
||||
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
||||
),
|
||||
filterEnabled: config.filterEnabled,
|
||||
postWidgetBuilder: config.postWidgetBuilder,
|
||||
);
|
||||
|
||||
var button = FloatingActionButton(
|
||||
backgroundColor: config
|
||||
.optionsBuilder(context)
|
||||
.theme
|
||||
.postCreationFloatingActionButtonColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
onPressed: () async => context.push(
|
||||
TimelineUserStoryRoutes.timelineCategorySelection,
|
||||
),
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: config.homeOpenPageBuilder
|
||||
?.call(context, timelineScreen, button) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config
|
||||
.optionsBuilder(context)
|
||||
.translations
|
||||
.timeLineScreenTitle,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: timelineScreen,
|
||||
floatingActionButton: button,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelineCategorySelection,
|
||||
pageBuilder: (context, state) {
|
||||
var timelineSelectionScreen = TimelineSelectionScreen(
|
||||
options: config.optionsBuilder(context),
|
||||
categories: config
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context) ??
|
||||
[],
|
||||
onCategorySelected: (category) async {
|
||||
await context.push(
|
||||
TimelineUserStoryRoutes.timelinepostCreation(category.key ?? ''),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
var backButton = IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: config.categorySelectionOpenPageBuilder
|
||||
?.call(context, timelineSelectionScreen) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: timelineSelectionScreen,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelineView,
|
||||
pageBuilder: (context, state) {
|
||||
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.getUserId?.call(context) ?? config.userId,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
options: config.optionsBuilder(context),
|
||||
service: service,
|
||||
post: 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),
|
||||
);
|
||||
|
||||
var backButton = IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: config.postViewOpenPageBuilder?.call(
|
||||
context,
|
||||
timelinePostWidget,
|
||||
backButton,
|
||||
post,
|
||||
category,
|
||||
) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
category?.title ?? post.category ?? 'Category',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: timelinePostWidget,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: TimelineUserStoryRoutes.timelinePostCreation,
|
||||
pageBuilder: (context, state) {
|
||||
var category = state.pathParameters['category'];
|
||||
var service = config.serviceBuilder?.call(context) ?? config.service;
|
||||
var timelinePostCreationWidget = TimelinePostCreationScreen(
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
options: config.optionsBuilder(context),
|
||||
service: service,
|
||||
onPostCreated: (post) async {
|
||||
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(
|
||||
TimelineUserStoryRoutes.timelinePostOverview,
|
||||
extra: post,
|
||||
),
|
||||
enablePostOverviewScreen: config.enablePostOverviewScreen,
|
||||
postCategory: category,
|
||||
);
|
||||
|
||||
var backButton = IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () =>
|
||||
context.go(TimelineUserStoryRoutes.timelineCategorySelection),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: config.postCreationOpenPageBuilder
|
||||
?.call(context, timelinePostCreationWidget, backButton) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xff212121),
|
||||
leading: backButton,
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: timelinePostCreationWidget,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
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: service,
|
||||
timelinePost: post,
|
||||
onPostSubmit: (post) async {
|
||||
await service.postService.createPost(post);
|
||||
if (!context.mounted) return;
|
||||
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||
},
|
||||
);
|
||||
var backButton = IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () async => context.pop(),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
state: state,
|
||||
child: config.postOverviewOpenPageBuilder?.call(
|
||||
context,
|
||||
timelinePostOverviewWidget,
|
||||
) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postOverview,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: timelinePostOverviewWidget,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
|
@ -10,11 +10,13 @@ import 'package:flutter_timeline/flutter_timeline.dart';
|
|||
/// This function creates a navigator for displaying user stories on a timeline.
|
||||
/// It takes a [BuildContext] and an optional [TimelineUserStoryConfiguration]
|
||||
/// as parameters. If no configuration is provided, default values will be used.
|
||||
late TimelineUserStoryConfiguration timelineUserStoryConfiguration;
|
||||
|
||||
Widget timeLineNavigatorUserStory({
|
||||
required BuildContext context,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
timelineUserStoryConfiguration = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
|
@ -23,7 +25,10 @@ Widget timeLineNavigatorUserStory({
|
|||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
return _timelineScreenRoute(configuration: config, context: context);
|
||||
return _timelineScreenRoute(
|
||||
config: timelineUserStoryConfiguration,
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
|
||||
/// A widget function that creates a timeline screen route.
|
||||
|
@ -33,18 +38,11 @@ Widget timeLineNavigatorUserStory({
|
|||
/// parameters. If no configuration is provided, default values will be used.
|
||||
Widget _timelineScreenRoute({
|
||||
required BuildContext context,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
required TimelineUserStoryConfiguration config,
|
||||
String? initalCategory,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
var timelineScreen = TimelineScreen(
|
||||
timelineCategory: initalCategory,
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
|
@ -55,7 +53,7 @@ Widget _timelineScreenRoute({
|
|||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postDetailScreenRoute(
|
||||
configuration: config,
|
||||
config: config,
|
||||
context: context,
|
||||
post: post,
|
||||
),
|
||||
|
@ -65,40 +63,50 @@ Widget _timelineScreenRoute({
|
|||
filterEnabled: config.filterEnabled,
|
||||
postWidgetBuilder: config.postWidgetBuilder,
|
||||
);
|
||||
|
||||
var theme = Theme.of(context);
|
||||
var button = FloatingActionButton(
|
||||
backgroundColor: config
|
||||
.optionsBuilder(context)
|
||||
.theme
|
||||
.postCreationFloatingActionButtonColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
onPressed: () async => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postCategorySelectionScreen(
|
||||
configuration: config,
|
||||
context: context,
|
||||
),
|
||||
),
|
||||
),
|
||||
theme.colorScheme.primary,
|
||||
onPressed: () async {
|
||||
var selectedCategory = config.service.postService.selectedCategory;
|
||||
if (selectedCategory != null && selectedCategory.key != null) {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postCreationScreenRoute(
|
||||
config: config,
|
||||
context: context,
|
||||
category: selectedCategory,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postCategorySelectionScreen(
|
||||
config: config,
|
||||
context: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
|
||||
return config.homeOpenPageBuilder?.call(context, timelineScreen, button) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.timeLineScreenTitle,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
),
|
||||
body: timelineScreen,
|
||||
|
@ -115,17 +123,8 @@ Widget _timelineScreenRoute({
|
|||
Widget _postDetailScreenRoute({
|
||||
required BuildContext context,
|
||||
required TimelinePost post,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
required TimelineUserStoryConfiguration config,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
var timelinePostScreen = TimelinePostScreen(
|
||||
userId: config.getUserId?.call(context) ?? config.userId,
|
||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
||||
|
@ -143,11 +142,7 @@ Widget _postDetailScreenRoute({
|
|||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
);
|
||||
|
||||
var category = config
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context)
|
||||
var category = config.service.postService.categories
|
||||
.firstWhere((element) => element.key == post.category);
|
||||
|
||||
var backButton = IconButton(
|
||||
|
@ -160,10 +155,9 @@ Widget _postDetailScreenRoute({
|
|||
?.call(context, timelinePostScreen, backButton, post, category) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
||||
title: Text(
|
||||
category?.title ?? post.category ?? 'Category',
|
||||
category.title.toLowerCase(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
|
@ -183,31 +177,24 @@ Widget _postDetailScreenRoute({
|
|||
Widget _postCreationScreenRoute({
|
||||
required BuildContext context,
|
||||
required TimelineCategory category,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
required TimelineUserStoryConfiguration config,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
var timelinePostCreationScreen = TimelinePostCreationScreen(
|
||||
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) return;
|
||||
if (config.afterPostCreationGoHome) {
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _timelineScreenRoute(
|
||||
configuration: config,
|
||||
config: config,
|
||||
context: context,
|
||||
initalCategory: category.title,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -216,7 +203,7 @@ Widget _postCreationScreenRoute({
|
|||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postOverviewScreenRoute(
|
||||
configuration: config,
|
||||
config: config,
|
||||
context: context,
|
||||
post: newPost,
|
||||
),
|
||||
|
@ -227,7 +214,7 @@ Widget _postCreationScreenRoute({
|
|||
onPostOverview: (post) async => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postOverviewScreenRoute(
|
||||
configuration: config,
|
||||
config: config,
|
||||
context: context,
|
||||
post: post,
|
||||
),
|
||||
|
@ -249,7 +236,7 @@ Widget _postCreationScreenRoute({
|
|||
?.call(context, timelinePostCreationScreen, backButton) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xff212121),
|
||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
||||
leading: backButton,
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
|
@ -273,28 +260,23 @@ Widget _postCreationScreenRoute({
|
|||
Widget _postOverviewScreenRoute({
|
||||
required BuildContext context,
|
||||
required TimelinePost post,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
required TimelineUserStoryConfiguration config,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
var timelinePostOverviewWidget = TimelinePostOverviewScreen(
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
timelinePost: post,
|
||||
onPostSubmit: (post) async {
|
||||
await config.service.postService.createPost(post);
|
||||
var createdPost = await config.service.postService.createPost(post);
|
||||
config.onPostCreate?.call(createdPost);
|
||||
if (context.mounted) {
|
||||
await Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
_timelineScreenRoute(configuration: config, context: context),
|
||||
builder: (context) => _timelineScreenRoute(
|
||||
config: config,
|
||||
context: context,
|
||||
initalCategory: post.category,
|
||||
),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
|
@ -316,10 +298,10 @@ Widget _postOverviewScreenRoute({
|
|||
) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postOverview,
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 24,
|
||||
|
@ -333,30 +315,17 @@ Widget _postOverviewScreenRoute({
|
|||
|
||||
Widget _postCategorySelectionScreen({
|
||||
required BuildContext context,
|
||||
TimelineUserStoryConfiguration? configuration,
|
||||
required TimelineUserStoryConfiguration config,
|
||||
}) {
|
||||
var config = configuration ??
|
||||
TimelineUserStoryConfiguration(
|
||||
userId: 'test_user',
|
||||
service: TimelineService(
|
||||
postService: LocalTimelinePostService(),
|
||||
),
|
||||
optionsBuilder: (context) => const TimelineOptions(),
|
||||
);
|
||||
|
||||
var timelineSelectionScreen = TimelineSelectionScreen(
|
||||
postService: config.service.postService,
|
||||
options: config.optionsBuilder(context),
|
||||
categories: config
|
||||
.optionsBuilder(context)
|
||||
.categoriesOptions
|
||||
.categoriesBuilder
|
||||
?.call(context) ??
|
||||
[],
|
||||
categories: config.service.postService.categories,
|
||||
onCategorySelected: (category) async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postCreationScreenRoute(
|
||||
configuration: config,
|
||||
config: config,
|
||||
context: context,
|
||||
category: category,
|
||||
),
|
||||
|
@ -377,8 +346,8 @@ Widget _postCategorySelectionScreen({
|
|||
?.call(context, timelineSelectionScreen) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
||||
leading: backButton,
|
||||
backgroundColor: const Color(0xff212121),
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: TextStyle(
|
||||
|
@ -391,3 +360,15 @@ Widget _postCategorySelectionScreen({
|
|||
body: timelineSelectionScreen,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> routeToPostDetail(BuildContext context, TimelinePost post) async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _postDetailScreenRoute(
|
||||
config: timelineUserStoryConfiguration,
|
||||
context: context,
|
||||
post: post,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Iconica
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
CustomTransitionPage buildScreenWithFadeTransition<T>({
|
||||
required BuildContext context,
|
||||
required GoRouterState state,
|
||||
required Widget child,
|
||||
}) =>
|
||||
CustomTransitionPage<T>(
|
||||
key: state.pageKey,
|
||||
child: child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
|
||||
CustomTransitionPage buildScreenWithoutTransition<T>({
|
||||
required BuildContext context,
|
||||
required GoRouterState state,
|
||||
required Widget child,
|
||||
}) =>
|
||||
CustomTransitionPage<T>(
|
||||
key: state.pageKey,
|
||||
child: child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
child,
|
||||
);
|
|
@ -65,6 +65,7 @@ class TimelineUserStoryConfiguration {
|
|||
this.afterPostCreationGoHome = false,
|
||||
this.enablePostOverviewScreen = true,
|
||||
this.categorySelectionOpenPageBuilder,
|
||||
this.onPostCreate,
|
||||
});
|
||||
|
||||
/// The ID of the user associated with this user story configuration.
|
||||
|
@ -159,4 +160,6 @@ class TimelineUserStoryConfiguration {
|
|||
BuildContext context,
|
||||
Widget child,
|
||||
)? categorySelectionOpenPageBuilder;
|
||||
|
||||
final Function(TimelinePost post)? onPostCreate;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
name: flutter_timeline
|
||||
description: Visual elements and interface combined into one package
|
||||
version: 4.1.0
|
||||
|
||||
publish_to: none
|
||||
version: 5.1.0
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
|
||||
environment:
|
||||
sdk: ">=3.1.3 <4.0.0"
|
||||
|
@ -13,21 +12,14 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
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: 4.1.0
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^5.1.0
|
||||
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 4.1.0
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^5.1.0
|
||||
collection: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
|
1
packages/flutter_timeline_firebase/CHANGELOG.md
Symbolic link
1
packages/flutter_timeline_firebase/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/flutter_timeline_firebase/LICENSE
Symbolic link
1
packages/flutter_timeline_firebase/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
1
packages/flutter_timeline_firebase/README.md
Symbolic link
1
packages/flutter_timeline_firebase/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../README.md
|
|
@ -9,8 +9,10 @@ class FirebaseTimelineOptions {
|
|||
const FirebaseTimelineOptions({
|
||||
this.usersCollectionName = 'users',
|
||||
this.timelineCollectionName = 'timeline',
|
||||
this.timelineCategoryCollectionName = 'timeline_categories',
|
||||
});
|
||||
|
||||
final String usersCollectionName;
|
||||
final String timelineCollectionName;
|
||||
final String timelineCategoryCollectionName;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -38,6 +39,12 @@ class FirebaseTimelinePostService
|
|||
@override
|
||||
List<TimelinePost> posts = [];
|
||||
|
||||
@override
|
||||
List<TimelineCategory> categories = [];
|
||||
|
||||
@override
|
||||
TimelineCategory? selectedCategory;
|
||||
|
||||
@override
|
||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||
var postId = const Uuid().v4();
|
||||
|
@ -118,7 +125,6 @@ class FirebaseTimelinePostService
|
|||
|
||||
@override
|
||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
||||
debugPrint('fetching posts from firebase with category: $category');
|
||||
var snapshot = (category != null)
|
||||
? await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
|
@ -239,10 +245,20 @@ class FirebaseTimelinePostService
|
|||
}
|
||||
|
||||
@override
|
||||
TimelinePost? getPost(String postId) =>
|
||||
(posts.any((element) => element.id == postId))
|
||||
? posts.firstWhere((element) => element.id == postId)
|
||||
: null;
|
||||
Future<TimelinePost?> getPost(String postId) async {
|
||||
var post = await _db
|
||||
.collection(_options.timelineCollectionName)
|
||||
.doc(postId)
|
||||
.withConverter<TimelinePost>(
|
||||
fromFirestore: (snapshot, _) => TimelinePost.fromJson(
|
||||
snapshot.id,
|
||||
snapshot.data()!,
|
||||
),
|
||||
toFirestore: (user, _) => user.toJson(),
|
||||
)
|
||||
.get();
|
||||
return post.data();
|
||||
}
|
||||
|
||||
@override
|
||||
List<TimelinePost> getPosts(String? category) => posts
|
||||
|
@ -358,4 +374,122 @@ class FirebaseTimelinePostService
|
|||
|
||||
return user;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> addCategory(TimelineCategory category) async {
|
||||
var exists = categories.firstWhereOrNull(
|
||||
(element) => element.title.toLowerCase() == category.title.toLowerCase(),
|
||||
);
|
||||
if (exists != null) return false;
|
||||
try {
|
||||
await _db
|
||||
.collection(_options.timelineCategoryCollectionName)
|
||||
.add(category.toJson());
|
||||
categories.add(category);
|
||||
notifyListeners();
|
||||
return true;
|
||||
} on Exception catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TimelineCategory>> fetchCategories() async {
|
||||
categories.clear();
|
||||
categories.add(
|
||||
const TimelineCategory(
|
||||
key: null,
|
||||
title: 'All',
|
||||
),
|
||||
);
|
||||
var categoriesSnapshot = await _db
|
||||
.collection(_options.timelineCategoryCollectionName)
|
||||
.withConverter(
|
||||
fromFirestore: (snapshot, _) =>
|
||||
TimelineCategory.fromJson(snapshot.data()!),
|
||||
toFirestore: (model, _) => model.toJson(),
|
||||
)
|
||||
.get();
|
||||
categories.addAll(categoriesSnapshot.docs.map((e) => e.data()));
|
||||
|
||||
notifyListeners();
|
||||
return categories;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> likeReaction(
|
||||
String userId,
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
) async {
|
||||
// update the post with the new like
|
||||
var updatedPost = post.copyWith(
|
||||
reactions: post.reactions?.map(
|
||||
(r) {
|
||||
if (r.id == reactionId) {
|
||||
return r.copyWith(
|
||||
likedBy: (r.likedBy ?? [])..add(userId),
|
||||
);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.update({
|
||||
'reactions': post.reactions
|
||||
?.map(
|
||||
(r) =>
|
||||
r.id == reactionId ? r.copyWith(likedBy: r.likedBy ?? []) : r,
|
||||
)
|
||||
.map((e) => e.toJson())
|
||||
.toList(),
|
||||
});
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> unlikeReaction(
|
||||
String userId,
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
) async {
|
||||
// update the post with the new like
|
||||
var updatedPost = post.copyWith(
|
||||
reactions: post.reactions?.map(
|
||||
(r) {
|
||||
if (r.id == reactionId) {
|
||||
return r.copyWith(
|
||||
likedBy: r.likedBy?..remove(userId),
|
||||
);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||
await postRef.update({
|
||||
'reactions': post.reactions
|
||||
?.map(
|
||||
(r) => r.id == reactionId
|
||||
? r.copyWith(likedBy: r.likedBy?..remove(userId))
|
||||
: r,
|
||||
)
|
||||
.map((e) => e.toJson())
|
||||
.toList(),
|
||||
});
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
|
||||
name: flutter_timeline_firebase
|
||||
description: Implementation of the Flutter Timeline interface for Firebase.
|
||||
version: 4.1.0
|
||||
|
||||
publish_to: none
|
||||
version: 5.1.0
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.3 <4.0.0'
|
||||
sdk: ">=3.1.3 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
@ -18,12 +17,10 @@ dependencies:
|
|||
firebase_core: ^2.22.0
|
||||
firebase_storage: ^11.5.1
|
||||
uuid: ^4.2.1
|
||||
|
||||
collection: ^1.18.0
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 4.1.0
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^5.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
@ -33,4 +30,3 @@ dev_dependencies:
|
|||
ref: 6.0.0
|
||||
|
||||
flutter:
|
||||
|
||||
|
|
1
packages/flutter_timeline_interface/CHANGELOG.md
Symbolic link
1
packages/flutter_timeline_interface/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/flutter_timeline_interface/LICENSE
Symbolic link
1
packages/flutter_timeline_interface/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
1
packages/flutter_timeline_interface/README.md
Symbolic link
1
packages/flutter_timeline_interface/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../README.md
|
|
@ -5,13 +5,44 @@ class TimelineCategory {
|
|||
const TimelineCategory({
|
||||
required this.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.icon,
|
||||
this.canCreate = true,
|
||||
this.canView = true,
|
||||
});
|
||||
|
||||
TimelineCategory.fromJson(Map<String, dynamic> json)
|
||||
: key = json['key'] as String?,
|
||||
title = json['title'] as String,
|
||||
icon = json['icon'] as Widget?,
|
||||
canCreate = json['canCreate'] as bool? ?? true,
|
||||
canView = json['canView'] as bool? ?? true;
|
||||
|
||||
final String? key;
|
||||
final String title;
|
||||
final Widget icon;
|
||||
final Widget? icon;
|
||||
final bool canCreate;
|
||||
final bool canView;
|
||||
|
||||
TimelineCategory copyWith({
|
||||
String? key,
|
||||
String? title,
|
||||
Widget? icon,
|
||||
bool? canCreate,
|
||||
bool? canView,
|
||||
}) =>
|
||||
TimelineCategory(
|
||||
key: key ?? this.key,
|
||||
title: title ?? this.title,
|
||||
icon: icon ?? this.icon,
|
||||
canCreate: canCreate ?? this.canCreate,
|
||||
canView: canView ?? this.canView,
|
||||
);
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'key': key,
|
||||
'title': title,
|
||||
'icon': icon,
|
||||
'canCreate': canCreate,
|
||||
'canView': canView,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,11 +13,28 @@ class TimelinePosterUserModel {
|
|||
this.imageUrl,
|
||||
});
|
||||
|
||||
factory TimelinePosterUserModel.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
String userId,
|
||||
) =>
|
||||
TimelinePosterUserModel(
|
||||
userId: userId,
|
||||
firstName: json['first_name'] as String?,
|
||||
lastName: json['last_name'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
);
|
||||
|
||||
final String userId;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? imageUrl;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'image_url': imageUrl,
|
||||
};
|
||||
|
||||
String? get fullName {
|
||||
var fullName = '';
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ class TimelinePostReaction {
|
|||
this.imageUrl,
|
||||
this.creator,
|
||||
this.createdAtString,
|
||||
this.likedBy,
|
||||
});
|
||||
|
||||
factory TimelinePostReaction.fromJson(
|
||||
|
@ -31,6 +32,7 @@ class TimelinePostReaction {
|
|||
imageUrl: json['image_url'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
createdAtString: json['created_at'] as String,
|
||||
likedBy: (json['liked_by'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||
);
|
||||
|
||||
/// The unique identifier of the reaction.
|
||||
|
@ -57,6 +59,8 @@ class TimelinePostReaction {
|
|||
/// Reaction creation date as String with microseconds.
|
||||
final String? createdAtString;
|
||||
|
||||
final List<String>? likedBy;
|
||||
|
||||
TimelinePostReaction copyWith({
|
||||
String? id,
|
||||
String? postId,
|
||||
|
@ -65,6 +69,7 @@ class TimelinePostReaction {
|
|||
String? reaction,
|
||||
String? imageUrl,
|
||||
DateTime? createdAt,
|
||||
List<String>? likedBy,
|
||||
}) =>
|
||||
TimelinePostReaction(
|
||||
id: id ?? this.id,
|
||||
|
@ -74,6 +79,7 @@ class TimelinePostReaction {
|
|||
reaction: reaction ?? this.reaction,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
likedBy: likedBy ?? this.likedBy,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
|
@ -82,6 +88,7 @@ class TimelinePostReaction {
|
|||
'reaction': reaction,
|
||||
'image_url': imageUrl,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'liked_by': likedBy,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -91,6 +98,7 @@ class TimelinePostReaction {
|
|||
'reaction': reaction,
|
||||
'image_url': imageUrl,
|
||||
'created_at': createdAtString,
|
||||
'liked_by': likedBy,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
|
||||
abstract class TimelinePostService with ChangeNotifier {
|
||||
List<TimelinePost> posts = [];
|
||||
List<TimelineCategory> categories = [];
|
||||
TimelineCategory? selectedCategory;
|
||||
|
||||
Future<void> deletePost(TimelinePost post);
|
||||
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
||||
|
@ -17,7 +18,7 @@ abstract class TimelinePostService with ChangeNotifier {
|
|||
Future<List<TimelinePost>> fetchPosts(String? category);
|
||||
Future<TimelinePost> fetchPost(TimelinePost post);
|
||||
Future<List<TimelinePost>> fetchPostsPaginated(String? category, int limit);
|
||||
TimelinePost? getPost(String postId);
|
||||
Future<TimelinePost?> getPost(String postId);
|
||||
List<TimelinePost> getPosts(String? category);
|
||||
Future<List<TimelinePost>> refreshPosts(String? category);
|
||||
Future<TimelinePost> fetchPostDetails(TimelinePost post);
|
||||
|
@ -28,4 +29,17 @@ abstract class TimelinePostService with ChangeNotifier {
|
|||
});
|
||||
Future<TimelinePost> likePost(String userId, TimelinePost post);
|
||||
Future<TimelinePost> unlikePost(String userId, TimelinePost post);
|
||||
|
||||
Future<List<TimelineCategory>> fetchCategories();
|
||||
Future<bool> addCategory(TimelineCategory category);
|
||||
Future<TimelinePost> likeReaction(
|
||||
String userId,
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
);
|
||||
Future<TimelinePost> unlikeReaction(
|
||||
String userId,
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
|
||||
name: flutter_timeline_interface
|
||||
description: Interface for the service of the Flutter Timeline component
|
||||
version: 4.1.0
|
||||
|
||||
publish_to: none
|
||||
version: 5.1.0
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.3 <4.0.0'
|
||||
|
|
1
packages/flutter_timeline_view/CHANGELOG.md
Symbolic link
1
packages/flutter_timeline_view/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/flutter_timeline_view/LICENSE
Symbolic link
1
packages/flutter_timeline_view/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
1
packages/flutter_timeline_view/README.md
Symbolic link
1
packages/flutter_timeline_view/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../README.md
|
3
packages/flutter_timeline_view/assets/Comment.svg
Normal file
3
packages/flutter_timeline_view/assets/Comment.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5 3.5C14.3587 3.5 17.5 6.64125 17.5 10.5C17.5 14.3587 14.3587 17.5 10.5 17.5C9.4675 17.5 8.4525 17.2725 7.49875 16.8175C7.2625 16.7037 7.00875 16.6513 6.74625 16.6513C6.58 16.6513 6.41375 16.6775 6.25625 16.7213L3.45625 17.5437L4.27875 14.7438C4.40125 14.3325 4.36625 13.8863 4.1825 13.5013C3.7275 12.5475 3.5 11.5325 3.5 10.5C3.5 6.64125 6.64125 3.5 10.5 3.5ZM10.5 1.75C5.67 1.75 1.75 5.67 1.75 10.5C1.75 11.8475 2.065 13.1075 2.59875 14.2538L0.875 20.125L6.74625 18.4013C7.8925 18.935 9.1525 19.25 10.5 19.25C15.33 19.25 19.25 15.33 19.25 10.5C19.25 5.67 15.33 1.75 10.5 1.75Z" fill="#212121"/>
|
||||
</svg>
|
After Width: | Height: | Size: 713 B |
3
packages/flutter_timeline_view/assets/send.svg
Normal file
3
packages/flutter_timeline_view/assets/send.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.05813 12.3603L7.52523 16.4769L11.6405 22.944C12.0627 23.6073 12.7875 23.9999 13.5628 23.9999C13.6281 23.9999 13.6921 23.9974 13.7585 23.9913C14.6077 23.9186 15.3399 23.3858 15.6697 22.6006L23.8204 3.1673C24.1809 2.30831 23.989 1.3287 23.3318 0.6703C22.6734 0.0118984 21.6938 -0.181315 20.8348 0.179268L1.39902 8.33114C0.612628 8.66096 0.0797531 9.3932 0.008375 10.2424C-0.0642338 11.0915 0.339422 11.9037 1.05813 12.3603ZM2.1128 10.0331L21.5473 1.8825C21.6113 1.85542 21.6704 1.84435 21.7233 1.84435C21.8735 1.84435 21.9793 1.92926 22.0248 1.97603C22.0876 2.03756 22.2205 2.20862 22.1184 2.45352L13.9677 21.8881C13.8754 22.1084 13.6822 22.1465 13.6022 22.1539C13.5222 22.1588 13.3241 22.1539 13.1973 21.9533L9.36876 15.9378L14.4674 10.8392C14.828 10.4786 14.828 9.89408 14.4674 9.53349C14.1068 9.17291 13.5222 9.17291 13.1616 9.53349L8.06303 14.6321L2.04757 10.8035C1.85436 10.6805 1.83836 10.4971 1.84698 10.3986C1.85436 10.3199 1.89251 10.1254 2.1128 10.0331Z" fill="#212121"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -16,10 +16,10 @@ class TimelineOptions {
|
|||
this.translations = const TimelineTranslations.empty(),
|
||||
this.paddings = const TimelinePaddingOptions(),
|
||||
this.imagePickerConfig = const ImagePickerConfig(),
|
||||
this.imagePickerTheme = const ImagePickerTheme(),
|
||||
this.imagePickerTheme,
|
||||
this.timelinePostHeight,
|
||||
this.sortCommentsAscending = true,
|
||||
this.sortPostsAscending,
|
||||
this.sortPostsAscending = false,
|
||||
this.doubleTapTolike = false,
|
||||
this.iconsWithValues = false,
|
||||
this.likeAndDislikeIconsForDoubleTap = const (
|
||||
|
@ -38,7 +38,7 @@ class TimelineOptions {
|
|||
this.userAvatarBuilder,
|
||||
this.anonymousAvatarBuilder,
|
||||
this.nameBuilder,
|
||||
this.iconSize = 26,
|
||||
this.iconSize = 24,
|
||||
this.postWidgetHeight,
|
||||
this.filterOptions = const FilterOptions(),
|
||||
this.categoriesOptions = const CategoriesOptions(),
|
||||
|
@ -93,7 +93,7 @@ class TimelineOptions {
|
|||
|
||||
/// ImagePickerTheme can be used to change the UI of the
|
||||
/// Image Picker Widget to change the text/icons to your liking.
|
||||
final ImagePickerTheme imagePickerTheme;
|
||||
final ImagePickerTheme? imagePickerTheme;
|
||||
|
||||
/// ImagePickerConfig can be used to define the
|
||||
/// size and quality for the uploaded image.
|
||||
|
@ -160,6 +160,7 @@ class TimelineOptions {
|
|||
BuildContext context,
|
||||
Function() onPressed,
|
||||
String text,
|
||||
TimelinePost post,
|
||||
)? postOverviewButtonBuilder;
|
||||
|
||||
/// Optional builder to override the default alertdialog for post deletion
|
||||
|
@ -174,53 +175,14 @@ class TimelineOptions {
|
|||
final InputDecoration? contentInputDecoration;
|
||||
}
|
||||
|
||||
List<TimelineCategory> _getDefaultCategories(context) => [
|
||||
const TimelineCategory(
|
||||
key: null,
|
||||
title: 'All',
|
||||
icon: Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.apps,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const TimelineCategory(
|
||||
key: 'Category',
|
||||
title: 'Category',
|
||||
icon: Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.category,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const TimelineCategory(
|
||||
key: 'Category with two lines',
|
||||
title: 'Category with two lines',
|
||||
icon: Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.category,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
class CategoriesOptions {
|
||||
const CategoriesOptions({
|
||||
this.categoriesBuilder = _getDefaultCategories,
|
||||
this.categoryButtonBuilder,
|
||||
this.categorySelectorHorizontalPadding,
|
||||
});
|
||||
|
||||
/// List of categories that the user can select.
|
||||
/// If this is null no categories will be shown.
|
||||
final List<TimelineCategory> Function(BuildContext context)?
|
||||
categoriesBuilder;
|
||||
|
||||
/// Abilty to override the standard category selector
|
||||
final Widget Function(
|
||||
|
@ -235,17 +197,11 @@ class CategoriesOptions {
|
|||
final double? categorySelectorHorizontalPadding;
|
||||
|
||||
TimelineCategory? getCategoryByKey(
|
||||
List<TimelineCategory> categories,
|
||||
BuildContext context,
|
||||
String? key,
|
||||
) {
|
||||
if (categoriesBuilder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return categoriesBuilder!
|
||||
.call(context)
|
||||
.firstWhereOrNull((category) => category.key == key);
|
||||
}
|
||||
) =>
|
||||
categories.firstWhereOrNull((category) => category.key == key);
|
||||
}
|
||||
|
||||
class FilterOptions {
|
||||
|
|
|
@ -4,9 +4,9 @@ import 'package:flutter/material.dart';
|
|||
class TimelinePaddingOptions {
|
||||
const TimelinePaddingOptions({
|
||||
this.mainPadding =
|
||||
const EdgeInsets.only(left: 12.0, top: 24.0, right: 12.0, bottom: 12.0),
|
||||
const EdgeInsets.only(left: 32, top: 20, right: 32, bottom: 40),
|
||||
this.postPadding =
|
||||
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
|
||||
const EdgeInsets.only(left: 12.0, top: 12, right: 12.0, bottom: 8),
|
||||
this.postOverviewButtonBottomPadding = 30.0,
|
||||
this.categoryButtonTextPadding,
|
||||
});
|
||||
|
|
|
@ -28,7 +28,6 @@ class TimelineTranslations {
|
|||
required this.allowCommentsDescription,
|
||||
required this.commentsTitleOnPost,
|
||||
required this.checkPost,
|
||||
required this.postAt,
|
||||
required this.deletePost,
|
||||
required this.deleteReaction,
|
||||
required this.deleteConfirmationMessage,
|
||||
|
@ -36,7 +35,8 @@ class TimelineTranslations {
|
|||
required this.deleteCancelButton,
|
||||
required this.deleteButton,
|
||||
required this.viewPost,
|
||||
required this.likesTitle,
|
||||
required this.oneLikeTitle,
|
||||
required this.multipleLikesTitle,
|
||||
required this.commentsTitle,
|
||||
required this.firstComment,
|
||||
required this.writeComment,
|
||||
|
@ -49,6 +49,14 @@ class TimelineTranslations {
|
|||
required this.yes,
|
||||
required this.no,
|
||||
required this.timeLineScreenTitle,
|
||||
required this.createCategoryPopuptitle,
|
||||
required this.addCategoryTitle,
|
||||
required this.addCategorySubmitButton,
|
||||
required this.addCategoryCancelButtton,
|
||||
required this.addCategoryHintText,
|
||||
required this.addCategoryErrorText,
|
||||
required this.titleErrorText,
|
||||
required this.contentErrorText,
|
||||
});
|
||||
|
||||
/// Default translations for the timeline component view
|
||||
|
@ -67,7 +75,7 @@ class TimelineTranslations {
|
|||
this.allowCommentsDescription =
|
||||
'Indicate whether people are allowed to respond',
|
||||
this.commentsTitleOnPost = 'Comments',
|
||||
this.checkPost = 'Check post overview',
|
||||
this.checkPost = 'Overview',
|
||||
this.deletePost = 'Delete post',
|
||||
this.deleteConfirmationTitle = 'Delete Post',
|
||||
this.deleteConfirmationMessage =
|
||||
|
@ -76,20 +84,28 @@ class TimelineTranslations {
|
|||
this.deleteCancelButton = 'Cancel',
|
||||
this.deleteReaction = 'Delete Reaction',
|
||||
this.viewPost = 'View post',
|
||||
this.likesTitle = 'Likes',
|
||||
this.oneLikeTitle = 'like',
|
||||
this.multipleLikesTitle = 'likes',
|
||||
this.commentsTitle = 'Are people allowed to comment?',
|
||||
this.firstComment = 'Be the first to comment',
|
||||
this.writeComment = 'Write your comment here...',
|
||||
this.postAt = 'at',
|
||||
this.postLoadingError = 'Something went wrong while loading the post',
|
||||
this.timelineSelectionDescription = 'Choose a category',
|
||||
this.searchHint = 'Search...',
|
||||
this.postOverview = 'Post Overview',
|
||||
this.postIn = 'Post in',
|
||||
this.postIn = 'Post',
|
||||
this.postCreation = 'add post',
|
||||
this.yes = 'Yes',
|
||||
this.no = 'No',
|
||||
this.timeLineScreenTitle = 'iconinstagram',
|
||||
this.createCategoryPopuptitle = 'Choose a title for the new category',
|
||||
this.addCategoryTitle = 'Add category',
|
||||
this.addCategorySubmitButton = 'Add category',
|
||||
this.addCategoryCancelButtton = 'Cancel',
|
||||
this.addCategoryHintText = 'Category name...',
|
||||
this.addCategoryErrorText = 'Please enter a category name',
|
||||
this.titleErrorText = 'Please enter a title',
|
||||
this.contentErrorText = 'Please enter content',
|
||||
});
|
||||
|
||||
final String noPosts;
|
||||
|
@ -104,10 +120,11 @@ class TimelineTranslations {
|
|||
final String allowComments;
|
||||
final String allowCommentsDescription;
|
||||
final String checkPost;
|
||||
final String postAt;
|
||||
|
||||
final String titleHintText;
|
||||
final String contentHintText;
|
||||
final String titleErrorText;
|
||||
final String contentErrorText;
|
||||
|
||||
final String deletePost;
|
||||
final String deleteConfirmationTitle;
|
||||
|
@ -117,7 +134,8 @@ class TimelineTranslations {
|
|||
|
||||
final String deleteReaction;
|
||||
final String viewPost;
|
||||
final String likesTitle;
|
||||
final String oneLikeTitle;
|
||||
final String multipleLikesTitle;
|
||||
final String commentsTitle;
|
||||
final String commentsTitleOnPost;
|
||||
final String writeComment;
|
||||
|
@ -132,6 +150,13 @@ class TimelineTranslations {
|
|||
final String postIn;
|
||||
final String postCreation;
|
||||
|
||||
final String createCategoryPopuptitle;
|
||||
final String addCategoryTitle;
|
||||
final String addCategorySubmitButton;
|
||||
final String addCategoryCancelButtton;
|
||||
final String addCategoryHintText;
|
||||
final String addCategoryErrorText;
|
||||
|
||||
final String yes;
|
||||
final String no;
|
||||
final String timeLineScreenTitle;
|
||||
|
@ -150,7 +175,6 @@ class TimelineTranslations {
|
|||
String? allowCommentsDescription,
|
||||
String? commentsTitleOnPost,
|
||||
String? checkPost,
|
||||
String? postAt,
|
||||
String? deletePost,
|
||||
String? deleteConfirmationTitle,
|
||||
String? deleteConfirmationMessage,
|
||||
|
@ -158,7 +182,8 @@ class TimelineTranslations {
|
|||
String? deleteCancelButton,
|
||||
String? deleteReaction,
|
||||
String? viewPost,
|
||||
String? likesTitle,
|
||||
String? oneLikeTitle,
|
||||
String? multipleLikesTitle,
|
||||
String? commentsTitle,
|
||||
String? writeComment,
|
||||
String? firstComment,
|
||||
|
@ -173,6 +198,14 @@ class TimelineTranslations {
|
|||
String? yes,
|
||||
String? no,
|
||||
String? timeLineScreenTitle,
|
||||
String? createCategoryPopuptitle,
|
||||
String? addCategoryTitle,
|
||||
String? addCategorySubmitButton,
|
||||
String? addCategoryCancelButtton,
|
||||
String? addCategoryHintText,
|
||||
String? addCategoryErrorText,
|
||||
String? titleErrorText,
|
||||
String? contentErrorText,
|
||||
}) =>
|
||||
TimelineTranslations(
|
||||
noPosts: noPosts ?? this.noPosts,
|
||||
|
@ -189,7 +222,6 @@ class TimelineTranslations {
|
|||
allowCommentsDescription ?? this.allowCommentsDescription,
|
||||
commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost,
|
||||
checkPost: checkPost ?? this.checkPost,
|
||||
postAt: postAt ?? this.postAt,
|
||||
deletePost: deletePost ?? this.deletePost,
|
||||
deleteConfirmationTitle:
|
||||
deleteConfirmationTitle ?? this.deleteConfirmationTitle,
|
||||
|
@ -199,7 +231,8 @@ class TimelineTranslations {
|
|||
deleteCancelButton: deleteCancelButton ?? this.deleteCancelButton,
|
||||
deleteReaction: deleteReaction ?? this.deleteReaction,
|
||||
viewPost: viewPost ?? this.viewPost,
|
||||
likesTitle: likesTitle ?? this.likesTitle,
|
||||
oneLikeTitle: oneLikeTitle ?? this.oneLikeTitle,
|
||||
multipleLikesTitle: multipleLikesTitle ?? this.multipleLikesTitle,
|
||||
commentsTitle: commentsTitle ?? this.commentsTitle,
|
||||
writeComment: writeComment ?? this.writeComment,
|
||||
firstComment: firstComment ?? this.firstComment,
|
||||
|
@ -215,5 +248,16 @@ class TimelineTranslations {
|
|||
yes: yes ?? this.yes,
|
||||
no: no ?? this.no,
|
||||
timeLineScreenTitle: timeLineScreenTitle ?? this.timeLineScreenTitle,
|
||||
addCategoryTitle: addCategoryTitle ?? this.addCategoryTitle,
|
||||
addCategorySubmitButton:
|
||||
addCategorySubmitButton ?? this.addCategorySubmitButton,
|
||||
addCategoryCancelButtton:
|
||||
addCategoryCancelButtton ?? this.addCategoryCancelButtton,
|
||||
addCategoryHintText: addCategoryHintText ?? this.addCategoryHintText,
|
||||
createCategoryPopuptitle:
|
||||
createCategoryPopuptitle ?? this.createCategoryPopuptitle,
|
||||
addCategoryErrorText: addCategoryErrorText ?? this.addCategoryErrorText,
|
||||
titleErrorText: titleErrorText ?? this.titleErrorText,
|
||||
contentErrorText: contentErrorText ?? this.contentErrorText,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import 'package:flutter_image_picker/flutter_image_picker.dart';
|
|||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/post_creation_textfield.dart';
|
||||
|
||||
class TimelinePostCreationScreen extends StatefulWidget {
|
||||
const TimelinePostCreationScreen({
|
||||
|
@ -51,52 +53,32 @@ class _TimelinePostCreationScreenState
|
|||
TextEditingController titleController = TextEditingController();
|
||||
TextEditingController contentController = TextEditingController();
|
||||
Uint8List? image;
|
||||
bool editingDone = false;
|
||||
bool allowComments = false;
|
||||
bool titleIsValid = false;
|
||||
bool contentIsValid = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
titleController.addListener(_listenForInputs);
|
||||
contentController.addListener(_listenForInputs);
|
||||
|
||||
super.initState();
|
||||
titleController.addListener(checkIfEditingDone);
|
||||
contentController.addListener(checkIfEditingDone);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
titleController.dispose();
|
||||
contentController.dispose();
|
||||
super.dispose();
|
||||
void _listenForInputs() {
|
||||
titleIsValid = titleController.text.isNotEmpty;
|
||||
contentIsValid = contentController.text.isNotEmpty;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void checkIfEditingDone() {
|
||||
setState(() {
|
||||
editingDone =
|
||||
titleController.text.isNotEmpty && contentController.text.isNotEmpty;
|
||||
if (widget.options.requireImageForPost) {
|
||||
editingDone = editingDone && image != null;
|
||||
}
|
||||
if (widget.options.minTitleLength != null) {
|
||||
editingDone = editingDone &&
|
||||
titleController.text.length >= widget.options.minTitleLength!;
|
||||
}
|
||||
if (widget.options.maxTitleLength != null) {
|
||||
editingDone = editingDone &&
|
||||
titleController.text.length <= widget.options.maxTitleLength!;
|
||||
}
|
||||
if (widget.options.minContentLength != null) {
|
||||
editingDone = editingDone &&
|
||||
contentController.text.length >= widget.options.minContentLength!;
|
||||
}
|
||||
if (widget.options.maxContentLength != null) {
|
||||
editingDone = editingDone &&
|
||||
contentController.text.length <= widget.options.maxContentLength!;
|
||||
}
|
||||
});
|
||||
}
|
||||
var formkey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var imageRequired = widget.options.requireImageForPost;
|
||||
|
||||
Future<void> onPostCreated() async {
|
||||
var user = await widget.service.userService?.getUser(widget.userId);
|
||||
var post = TimelinePost(
|
||||
id: 'Post${Random().nextInt(1000)}',
|
||||
creatorId: widget.userId,
|
||||
|
@ -109,6 +91,7 @@ class _TimelinePostCreationScreenState
|
|||
createdAt: DateTime.now(),
|
||||
reactionEnabled: allowComments,
|
||||
image: image,
|
||||
creator: user,
|
||||
);
|
||||
|
||||
if (widget.enablePostOverviewScreen) {
|
||||
|
@ -119,234 +102,291 @@ class _TimelinePostCreationScreenState
|
|||
}
|
||||
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Padding(
|
||||
padding: widget.options.paddings.mainPadding,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: widget.options.paddings.mainPadding,
|
||||
child: Form(
|
||||
key: formkey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.title,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
widget.options.textInputBuilder?.call(
|
||||
titleController,
|
||||
null,
|
||||
'',
|
||||
) ??
|
||||
TextField(
|
||||
maxLength: widget.options.maxTitleLength,
|
||||
controller: titleController,
|
||||
decoration: widget.options.contentInputDecoration ??
|
||||
InputDecoration(
|
||||
hintText: widget.options.translations.titleHintText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.options.translations.content,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.options.translations.contentDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// input field for the content
|
||||
TextField(
|
||||
controller: contentController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
expands: false,
|
||||
maxLines: null,
|
||||
minLines: null,
|
||||
decoration: widget.options.contentInputDecoration ??
|
||||
InputDecoration(
|
||||
hintText: widget.options.translations.contentHintText,
|
||||
widget.options.textInputBuilder?.call(
|
||||
titleController,
|
||||
null,
|
||||
'',
|
||||
) ??
|
||||
PostCreationTextfield(
|
||||
fieldKey: const ValueKey('title'),
|
||||
controller: titleController,
|
||||
hintText: widget.options.translations.titleHintText,
|
||||
textMaxLength: widget.options.maxTitleLength,
|
||||
decoration: widget.options.titleInputDecoration,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
expands: null,
|
||||
minLines: null,
|
||||
maxLines: 1,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return widget.options.translations.titleErrorText;
|
||||
}
|
||||
if (value.trim().isEmpty) {
|
||||
return widget.options.translations.titleErrorText;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
// input field for the content
|
||||
Text(
|
||||
widget.options.translations.uploadImage,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
widget.options.translations.content,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.uploadImageDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// image picker field
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// open a dialog to choose between camera and gallery
|
||||
var result = await showModalBottomSheet<Uint8List?>(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
color: theme.colorScheme.surface,
|
||||
child: ImagePicker(
|
||||
imagePickerConfig: widget.options.imagePickerConfig,
|
||||
imagePickerTheme: widget.options.imagePickerTheme,
|
||||
Text(
|
||||
widget.options.translations.contentDescription,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
PostCreationTextfield(
|
||||
fieldKey: const ValueKey('content'),
|
||||
controller: contentController,
|
||||
hintText: widget.options.translations.contentHintText,
|
||||
textMaxLength: null,
|
||||
decoration: widget.options.contentInputDecoration,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
expands: false,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return widget.options.translations.contentErrorText;
|
||||
}
|
||||
if (value.trim().isEmpty) {
|
||||
return widget.options.translations.contentErrorText;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.uploadImage,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.uploadImageDescription,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var result = await showModalBottomSheet<Uint8List?>(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: theme.colorScheme.surface,
|
||||
child: ImagePicker(
|
||||
config: widget.options.imagePickerConfig,
|
||||
theme: widget.options.imagePickerTheme ??
|
||||
ImagePickerTheme(
|
||||
titleStyle: theme.textTheme.titleMedium,
|
||||
iconSize: 40,
|
||||
selectImageText: 'UPLOAD FILE',
|
||||
makePhotoText: 'TAKE PICTURE',
|
||||
selectImageIcon: const Icon(
|
||||
size: 40,
|
||||
Icons.insert_drive_file,
|
||||
),
|
||||
closeButtonBuilder: (onTap) => TextButton(
|
||||
onPressed: () {
|
||||
onTap();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: theme.textTheme.bodyMedium!
|
||||
.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
image = result;
|
||||
});
|
||||
}
|
||||
checkIfEditingDone();
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: image != null
|
||||
? Image.memory(
|
||||
image!,
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
fit: BoxFit.cover,
|
||||
// give it a rounded border
|
||||
)
|
||||
: DottedBorder(
|
||||
dashPattern: const [4, 4],
|
||||
radius: const Radius.circular(8.0),
|
||||
color: theme.textTheme.displayMedium?.color ??
|
||||
Colors.white,
|
||||
child: const SizedBox(
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
image = result;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: image != null
|
||||
? Image.memory(
|
||||
image!,
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
size: 50,
|
||||
fit: BoxFit.cover,
|
||||
// give it a rounded border
|
||||
)
|
||||
: DottedBorder(
|
||||
dashPattern: const [4, 4],
|
||||
radius: const Radius.circular(8.0),
|
||||
color: theme.textTheme.displayMedium?.color ??
|
||||
Colors.white,
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// if an image is selected, show a delete button
|
||||
if (image != null) ...[
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
image = null;
|
||||
});
|
||||
checkIfEditingDone();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.white,
|
||||
// if an image is selected, show a delete button
|
||||
if (image != null) ...[
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
image = null;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.options.translations.commentsTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.allowCommentsDescription,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
activeColor: theme.colorScheme.primary,
|
||||
value: allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
allowComments = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.yes,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 32,
|
||||
),
|
||||
Checkbox(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
activeColor: theme.colorScheme.primary,
|
||||
value: !allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
allowComments = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.no,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
widget.options.translations.commentsTitle,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.allowCommentsDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
activeColor: theme.colorScheme.primary,
|
||||
value: allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
allowComments = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(widget.options.translations.yes),
|
||||
Checkbox(
|
||||
activeColor: theme.colorScheme.primary,
|
||||
value: !allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
allowComments = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(widget.options.translations.no),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 120),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: (widget.options.buttonBuilder != null)
|
||||
? widget.options.buttonBuilder!(
|
||||
context,
|
||||
onPostCreated,
|
||||
widget.options.translations.checkPost,
|
||||
enabled: editingDone,
|
||||
)
|
||||
: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.primary,
|
||||
const SizedBox(height: 120),
|
||||
SafeArea(
|
||||
bottom: true,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: widget.options.buttonBuilder?.call(
|
||||
context,
|
||||
onPostCreated,
|
||||
widget.options.translations.checkPost,
|
||||
enabled: formkey.currentState!.validate(),
|
||||
) ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 48),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultFilledButton(
|
||||
onPressed: titleIsValid &&
|
||||
contentIsValid &&
|
||||
(!imageRequired || image != null)
|
||||
? () async {
|
||||
if (formkey.currentState!
|
||||
.validate()) {
|
||||
await onPostCreated();
|
||||
await widget.service.postService
|
||||
.fetchPosts(null);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
buttonText: widget.enablePostOverviewScreen
|
||||
? widget.options.translations.checkPost
|
||||
: widget
|
||||
.options.translations.postCreation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: editingDone
|
||||
? () async {
|
||||
await onPostCreated();
|
||||
await widget.service.postService
|
||||
.fetchPosts(null);
|
||||
}
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
widget.enablePostOverviewScreen
|
||||
? widget.options.translations.checkPost
|
||||
: widget.options.translations.postCreation,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
// 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';
|
||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
||||
|
||||
class TimelinePostOverviewScreen extends StatelessWidget {
|
||||
const TimelinePostOverviewScreen({
|
||||
|
@ -20,13 +18,7 @@ class TimelinePostOverviewScreen extends StatelessWidget {
|
|||
|
||||
@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';
|
||||
var isSubmitted = false;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
|
@ -43,39 +35,43 @@ class TimelinePostOverviewScreen extends StatelessWidget {
|
|||
options.postOverviewButtonBuilder?.call(
|
||||
context,
|
||||
() {
|
||||
if (isSubmitted) return;
|
||||
isSubmitted = true;
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
buttonText,
|
||||
options.translations.postIn,
|
||||
timelinePost,
|
||||
) ??
|
||||
options.buttonBuilder?.call(
|
||||
context,
|
||||
() {
|
||||
if (isSubmitted) return;
|
||||
isSubmitted = true;
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
buttonText,
|
||||
options.translations.postIn,
|
||||
enabled: true,
|
||||
) ??
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
WidgetStatePropertyAll(Theme.of(context).primaryColor),
|
||||
),
|
||||
onPressed: () {
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
SafeArea(
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
buttonText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 80),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultFilledButton(
|
||||
onPressed: () async {
|
||||
if (isSubmitted) return;
|
||||
isSubmitted = true;
|
||||
onPostSubmit(timelinePost);
|
||||
},
|
||||
buttonText: options.translations.postIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: options.paddings.postOverviewButtonBottomPadding),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
|
||||
|
@ -60,20 +58,6 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
TimelinePost? post;
|
||||
bool isLoading = true;
|
||||
|
||||
late var textInputBuilder = widget.options.textInputBuilder ??
|
||||
(controller, suffixIcon, hintText) => TextField(
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(20.0), // Adjust the value as needed
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -90,8 +74,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
post = loadedPost;
|
||||
isLoading = false;
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
debugPrint('Error loading post: $e');
|
||||
} on Exception catch (_) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
|
@ -108,9 +91,10 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var dateFormat = widget.options.dateFormat ??
|
||||
DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode);
|
||||
var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm');
|
||||
|
||||
DateFormat(
|
||||
"dd/MM/yyyy 'at' HH:mm",
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
if (isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
|
@ -130,6 +114,45 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
? a.createdAt.compareTo(b.createdAt)
|
||||
: b.createdAt.compareTo(a.createdAt),
|
||||
);
|
||||
var isLikedByUser = post.likedBy?.contains(widget.userId) ?? false;
|
||||
|
||||
var textInputBuilder = widget.options.textInputBuilder ??
|
||||
(controller, suffixIcon, hintText) => TextField(
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 16,
|
||||
),
|
||||
hintText: widget.options.translations.writeComment,
|
||||
hintStyle: theme.textTheme.bodyMedium!.copyWith(
|
||||
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
|
||||
),
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(25),
|
||||
),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
),
|
||||
);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
|
@ -145,7 +168,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: widget.options.paddings.mainPadding,
|
||||
padding: widget.options.paddings.postPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -191,7 +214,9 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles
|
||||
.postCreatorTitleStyle ??
|
||||
theme.textTheme.titleMedium,
|
||||
theme.textTheme.titleSmall!.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -199,7 +224,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
const Spacer(),
|
||||
if (!(widget.isOverviewScreen ?? false) &&
|
||||
(widget.allowAllDeletion ||
|
||||
post.creator?.userId == widget.userId))
|
||||
post.creator?.userId == widget.userId)) ...[
|
||||
PopupMenuButton(
|
||||
onSelected: (value) async {
|
||||
if (value == 'delete') {
|
||||
|
@ -238,6 +263,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
// image of the posts
|
||||
|
@ -295,72 +321,70 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
// post information
|
||||
Row(
|
||||
children: [
|
||||
if (post.likedBy?.contains(widget.userId) ?? false) ...[
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () async {
|
||||
if (widget.isOverviewScreen ?? false) return;
|
||||
if (isLikedByUser) {
|
||||
updatePost(
|
||||
await widget.service.postService.unlikePost(
|
||||
widget.userId,
|
||||
post,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
widget.post.likedBy
|
||||
?.contains(widget.userId) ??
|
||||
false
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline_outlined,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
if (widget.isOverviewScreen ?? false) return;
|
||||
setState(() {});
|
||||
} else {
|
||||
updatePost(
|
||||
await widget.service.postService.likePost(
|
||||
widget.userId,
|
||||
post,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: widget.options.theme.likeIcon ??
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
icon: isLikedByUser
|
||||
? widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
widget.post.likedBy
|
||||
?.contains(widget.userId) ??
|
||||
false
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline_outlined,
|
||||
Icons.favorite_rounded,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
)
|
||||
: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
Icons.favorite_outline_outlined,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (post.reactionEnabled)
|
||||
widget.options.theme.commentIcon ??
|
||||
Icon(
|
||||
Icons.chat_bubble_outline_rounded,
|
||||
SvgPicture.asset(
|
||||
'assets/Comment.svg',
|
||||
package: 'flutter_timeline_view',
|
||||
// ignore: deprecated_member_use
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
width: widget.options.iconSize,
|
||||
height: widget.options.iconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${post.likes} ${widget.options.translations.likesTitle}',
|
||||
style: widget
|
||||
.options.theme.textStyles.postLikeTitleAndAmount ??
|
||||
theme.textTheme.titleSmall
|
||||
?.copyWith(color: Colors.black),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||
if (widget.isOverviewScreen != null
|
||||
? !widget.isOverviewScreen!
|
||||
: false) ...[
|
||||
Text(
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'${post.likes} ${post.likes > 1 ? widget.options.translations.multipleLikesTitle : widget.options.translations.oneLikeTitle}',
|
||||
style: widget.options.theme.textStyles
|
||||
.postLikeTitleAndAmount ??
|
||||
theme.textTheme.titleSmall
|
||||
?.copyWith(color: Colors.black),
|
||||
),
|
||||
],
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: widget.options.nameBuilder?.call(post.creator) ??
|
||||
|
@ -368,62 +392,42 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
widget.options.translations.anonymousUser,
|
||||
style: widget
|
||||
.options.theme.textStyles.postCreatorNameStyle ??
|
||||
theme.textTheme.titleSmall,
|
||||
theme.textTheme.titleSmall!
|
||||
.copyWith(color: Colors.black),
|
||||
children: [
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: post.title,
|
||||
style:
|
||||
widget.options.theme.textStyles.postTitleStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Html(
|
||||
data: post.content,
|
||||
style: {
|
||||
'body': Style(
|
||||
padding: HtmlPaddings.zero,
|
||||
margin: Margins.zero,
|
||||
),
|
||||
'#': Style(
|
||||
maxLines: 3,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
padding: HtmlPaddings.zero,
|
||||
margin: Margins.zero,
|
||||
),
|
||||
'H1': Style(
|
||||
padding: HtmlPaddings.zero,
|
||||
margin: Margins.zero,
|
||||
),
|
||||
'H2': Style(
|
||||
padding: HtmlPaddings.zero,
|
||||
margin: Margins.zero,
|
||||
),
|
||||
'H3': Style(
|
||||
padding: HtmlPaddings.zero,
|
||||
margin: Margins.zero,
|
||||
),
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${dateFormat.format(post.createdAt)} '
|
||||
'${widget.options.translations.postAt} '
|
||||
'${timeFormat.format(post.createdAt)}',
|
||||
post.content,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (post.reactionEnabled) ...[
|
||||
Text(
|
||||
'${dateFormat.format(post.createdAt)} ',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||
if (post.reactionEnabled && widget.isOverviewScreen != null
|
||||
? !widget.isOverviewScreen!
|
||||
: false) ...[
|
||||
Text(
|
||||
widget.options.translations.commentsTitleOnPost,
|
||||
style: theme.textTheme.titleMedium,
|
||||
style: theme.textTheme.titleSmall!
|
||||
.copyWith(color: Colors.black),
|
||||
),
|
||||
for (var reaction
|
||||
in post.reactions ?? <TimelinePostReaction>[]) ...[
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 4),
|
||||
GestureDetector(
|
||||
onLongPressStart: (details) async {
|
||||
if (reaction.creatorId == widget.userId ||
|
||||
|
@ -461,15 +465,13 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: reaction.imageUrl != null
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (reaction.creator?.imageUrl != null &&
|
||||
reaction.creator!.imageUrl!.isNotEmpty) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
reaction.creator!,
|
||||
28,
|
||||
14,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
|
@ -480,7 +482,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
] else ...[
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
reaction.creator!,
|
||||
28,
|
||||
14,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 14,
|
||||
|
@ -501,7 +503,8 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
reaction.creator?.fullName ??
|
||||
widget.options.translations
|
||||
.anonymousUser,
|
||||
style: theme.textTheme.titleSmall,
|
||||
style: theme.textTheme.titleSmall!
|
||||
.copyWith(color: Colors.black),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
@ -522,27 +525,91 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
reaction.creator?.fullName ??
|
||||
widget
|
||||
.options.translations.anonymousUser,
|
||||
style: theme.textTheme.titleSmall,
|
||||
style: theme.textTheme.titleSmall!
|
||||
.copyWith(color: Colors.black),
|
||||
children: [
|
||||
const TextSpan(text: ' '),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: reaction.reaction ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: dateFormat
|
||||
.format(reaction.createdAt),
|
||||
style: theme.textTheme.labelSmall!
|
||||
.copyWith(
|
||||
color: theme
|
||||
.textTheme.labelSmall!.color!
|
||||
.withOpacity(0.5),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
|
||||
// text should go to new line
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Builder(
|
||||
builder: (context) {
|
||||
var isLikedByUser =
|
||||
reaction.likedBy?.contains(widget.userId) ??
|
||||
false;
|
||||
return IconButton(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () async {
|
||||
if (isLikedByUser) {
|
||||
updatePost(
|
||||
await widget.service.postService
|
||||
.unlikeReaction(
|
||||
widget.userId,
|
||||
post,
|
||||
reaction.id,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
} else {
|
||||
updatePost(
|
||||
await widget.service.postService
|
||||
.likeReaction(
|
||||
widget.userId,
|
||||
post,
|
||||
reaction.id,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
icon: isLikedByUser
|
||||
? widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
Icons.favorite_rounded,
|
||||
color:
|
||||
widget.options.theme.iconColor,
|
||||
size: 14,
|
||||
)
|
||||
: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
Icons.favorite_outline_outlined,
|
||||
color:
|
||||
widget.options.theme.iconColor,
|
||||
size: 14,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
if (post.reactions?.isEmpty ?? true) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.options.translations.firstComment,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 120),
|
||||
|
@ -555,50 +622,71 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
if (post.reactionEnabled && !(widget.isOverviewScreen ?? false))
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ReactionBottom(
|
||||
messageInputBuilder: textInputBuilder,
|
||||
onPressSelectImage: () async {
|
||||
// open the image picker
|
||||
var result = await showModalBottomSheet<Uint8List?>(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
color: theme.colorScheme.surface,
|
||||
child: ImagePicker(
|
||||
imagePickerConfig: widget.options.imagePickerConfig,
|
||||
imagePickerTheme: widget.options.imagePickerTheme,
|
||||
child: Container(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width,
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: true,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: post.creator!.imageUrl != null
|
||||
? widget.options.userAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
),
|
||||
)
|
||||
: widget.options.anonymousAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
updatePost(
|
||||
await widget.service.postService.reactToPost(
|
||||
post,
|
||||
TimelinePostReaction(
|
||||
id: '',
|
||||
postId: post.id,
|
||||
creatorId: widget.userId,
|
||||
createdAt: DateTime.now(),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 16,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: ReactionBottom(
|
||||
messageInputBuilder: textInputBuilder,
|
||||
onReactionSubmit: (reaction) async => updatePost(
|
||||
await widget.service.postService.reactToPost(
|
||||
post,
|
||||
TimelinePostReaction(
|
||||
id: '',
|
||||
postId: post.id,
|
||||
reaction: reaction,
|
||||
creatorId: widget.userId,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
),
|
||||
),
|
||||
translations: widget.options.translations,
|
||||
iconColor: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
image: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onReactionSubmit: (reaction) async => updatePost(
|
||||
await widget.service.postService.reactToPost(
|
||||
post,
|
||||
TimelinePostReaction(
|
||||
id: '',
|
||||
postId: post.id,
|
||||
reaction: reaction,
|
||||
creatorId: widget.userId,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
translations: widget.options.translations,
|
||||
iconColor: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
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';
|
||||
|
@ -94,7 +95,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
|
||||
void _updateIsOnTop() {
|
||||
setState(() {
|
||||
_isOnTop = controller.position.pixels < 40;
|
||||
_isOnTop = controller.position.pixels < 0.1;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -148,6 +149,8 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
var categories = service.postService.categories;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -215,11 +218,16 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
),
|
||||
],
|
||||
CategorySelector(
|
||||
categories: categories,
|
||||
isOnTop: _isOnTop,
|
||||
filter: category,
|
||||
options: widget.options,
|
||||
onTapCategory: (categoryKey) {
|
||||
setState(() {
|
||||
service.postService.selectedCategory =
|
||||
categories.firstWhereOrNull(
|
||||
(element) => element.key == categoryKey,
|
||||
);
|
||||
category = categoryKey;
|
||||
});
|
||||
},
|
||||
|
@ -302,14 +310,14 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: widget.options.paddings.mainPadding.bottom,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: widget.options.paddings.mainPadding.bottom,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -319,6 +327,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
Future<void> loadPosts() async {
|
||||
if (widget.posts != null || !context.mounted) return;
|
||||
try {
|
||||
await service.postService.fetchCategories();
|
||||
await service.postService.fetchPosts(category);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/post_creation_textfield.dart';
|
||||
|
||||
class TimelineSelectionScreen extends StatelessWidget {
|
||||
class TimelineSelectionScreen extends StatefulWidget {
|
||||
const TimelineSelectionScreen({
|
||||
required this.options,
|
||||
required this.categories,
|
||||
required this.onCategorySelected,
|
||||
required this.postService,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -16,74 +19,196 @@ class TimelineSelectionScreen extends StatelessWidget {
|
|||
|
||||
final Function(TimelineCategory) onCategorySelected;
|
||||
|
||||
final TimelinePostService postService;
|
||||
|
||||
@override
|
||||
State<TimelineSelectionScreen> createState() =>
|
||||
_TimelineSelectionScreenState();
|
||||
}
|
||||
|
||||
class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var size = MediaQuery.of(context).size;
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: size.width * 0.05,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: size.height * 0.05, bottom: 8),
|
||||
child: Text(
|
||||
options.translations.timelineSelectionDescription,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 12),
|
||||
child: Text(
|
||||
widget.options.translations.timelineSelectionDescription,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
for (var category in categories.where(
|
||||
(element) => element.canCreate && element.key != null,
|
||||
)) ...[
|
||||
options.categorySelectorButtonBuilder?.call(
|
||||
context,
|
||||
() {
|
||||
onCategorySelected.call(category);
|
||||
},
|
||||
category.title,
|
||||
) ??
|
||||
InkWell(
|
||||
onTap: () => onCategorySelected.call(category),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color:
|
||||
options.theme.categorySelectionButtonBorderColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
width: 2,
|
||||
for (var category in widget.categories.where(
|
||||
(element) => element.canCreate && element.key != null,
|
||||
)) ...[
|
||||
widget.options.categorySelectorButtonBuilder?.call(
|
||||
context,
|
||||
() {
|
||||
widget.onCategorySelected.call(category);
|
||||
},
|
||||
category.title,
|
||||
) ??
|
||||
InkWell(
|
||||
onTap: () => widget.onCategorySelected.call(category),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: widget.options.theme
|
||||
.categorySelectionButtonBorderColor ??
|
||||
Theme.of(context).primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
color: widget.options.theme
|
||||
.categorySelectionButtonBackgroundColor,
|
||||
),
|
||||
color:
|
||||
options.theme.categorySelectionButtonBackgroundColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(
|
||||
category.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(
|
||||
category.title,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
InkWell(
|
||||
onTap: showCategoryPopup,
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: widget
|
||||
.options.theme.categorySelectionButtonBorderColor ??
|
||||
const Color(0xFF9E9E9E),
|
||||
width: 2,
|
||||
),
|
||||
color: widget
|
||||
.options.theme.categorySelectionButtonBackgroundColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: theme.textTheme.titleMedium?.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.options.translations.addCategoryTitle,
|
||||
style: theme.textTheme.titleMedium!.copyWith(
|
||||
color: theme.textTheme.titleMedium?.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showCategoryPopup() async {
|
||||
var theme = Theme.of(context);
|
||||
var controller = TextEditingController();
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 50, vertical: 24),
|
||||
titlePadding: const EdgeInsets.only(left: 44, right: 44, top: 32),
|
||||
title: Text(
|
||||
widget.options.translations.createCategoryPopuptitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PostCreationTextfield(
|
||||
controller: controller,
|
||||
hintText: widget.options.translations.addCategoryHintText,
|
||||
validator: (p0) => p0!.isEmpty
|
||||
? widget.options.translations.addCategoryErrorText
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: DefaultFilledButton(
|
||||
onPressed: () async {
|
||||
if (controller.text.isEmpty) return;
|
||||
await widget.postService.addCategory(
|
||||
TimelineCategory(
|
||||
key: controller.text,
|
||||
title: controller.text,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
buttonText:
|
||||
widget.options.translations.addCategorySubmitButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(
|
||||
widget.options.translations.addCategoryCancelButtton,
|
||||
style: theme.textTheme.bodyMedium!.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
color: theme.textTheme.bodyMedium?.color!.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,12 @@ class LocalTimelinePostService
|
|||
@override
|
||||
List<TimelinePost> posts = [];
|
||||
|
||||
@override
|
||||
List<TimelineCategory> categories = [];
|
||||
|
||||
@override
|
||||
TimelineCategory? selectedCategory;
|
||||
|
||||
@override
|
||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||
posts.add(
|
||||
|
@ -118,10 +124,11 @@ class LocalTimelinePostService
|
|||
}
|
||||
|
||||
@override
|
||||
TimelinePost? getPost(String postId) =>
|
||||
(posts.any((element) => element.id == postId))
|
||||
? posts.firstWhere((element) => element.id == postId)
|
||||
: null;
|
||||
Future<TimelinePost?> getPost(String postId) => Future.value(
|
||||
(posts.any((element) => element.id == postId))
|
||||
? posts.firstWhere((element) => element.id == postId)
|
||||
: null,
|
||||
);
|
||||
|
||||
@override
|
||||
List<TimelinePost> getPosts(String? category) => posts
|
||||
|
@ -241,4 +248,85 @@ class LocalTimelinePostService
|
|||
),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<bool> addCategory(TimelineCategory category) async {
|
||||
categories.add(category);
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TimelineCategory>> fetchCategories() async {
|
||||
categories = [
|
||||
const TimelineCategory(key: null, title: 'All'),
|
||||
const TimelineCategory(
|
||||
key: 'Category',
|
||||
title: 'Category',
|
||||
),
|
||||
const TimelineCategory(
|
||||
key: 'Category with two lines',
|
||||
title: 'Category with two lines',
|
||||
),
|
||||
];
|
||||
notifyListeners();
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> likeReaction(
|
||||
String userId,
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
) async {
|
||||
var updatedPost = post.copyWith(
|
||||
reactions: post.reactions?.map(
|
||||
(r) {
|
||||
if (r.id == reactionId) {
|
||||
return r.copyWith(
|
||||
likedBy: (r.likedBy ?? [])..add(userId),
|
||||
);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TimelinePost> unlikeReaction(
|
||||
String userId,
|
||||
TimelinePost post,
|
||||
String reactionId,
|
||||
) async {
|
||||
var updatedPost = post.copyWith(
|
||||
reactions: post.reactions?.map(
|
||||
(r) {
|
||||
if (r.id == reactionId) {
|
||||
return r.copyWith(
|
||||
likedBy: r.likedBy?..remove(userId),
|
||||
);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
posts = posts
|
||||
.map(
|
||||
(p) => p.id == post.id ? updatedPost : p,
|
||||
)
|
||||
.toList();
|
||||
|
||||
notifyListeners();
|
||||
return updatedPost;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||
|
||||
class CategorySelector extends StatefulWidget {
|
||||
|
@ -9,6 +10,7 @@ class CategorySelector extends StatefulWidget {
|
|||
required this.options,
|
||||
required this.onTapCategory,
|
||||
required this.isOnTop,
|
||||
required this.categories,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -16,6 +18,7 @@ class CategorySelector extends StatefulWidget {
|
|||
final TimelineOptions options;
|
||||
final void Function(String? categoryKey) onTapCategory;
|
||||
final bool isOnTop;
|
||||
final List<TimelineCategory> categories;
|
||||
|
||||
@override
|
||||
State<CategorySelector> createState() => _CategorySelectorState();
|
||||
|
@ -23,47 +26,42 @@ class CategorySelector extends StatefulWidget {
|
|||
|
||||
class _CategorySelectorState extends State<CategorySelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.options.categoriesOptions.categoriesBuilder == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
var categories =
|
||||
widget.options.categoriesOptions.categoriesBuilder!(context);
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.paddings.mainPadding.left - 20, 0),
|
||||
Widget build(BuildContext context) => SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.paddings.mainPadding.left - 20, 0),
|
||||
),
|
||||
for (var category in widget.categories) ...[
|
||||
widget.options.categoriesOptions.categoryButtonBuilder?.call(
|
||||
category,
|
||||
() => widget.onTapCategory(category.key),
|
||||
widget.filter == category.key,
|
||||
widget.isOnTop,
|
||||
) ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: CategorySelectorButton(
|
||||
isOnTop: widget.isOnTop,
|
||||
category: category,
|
||||
selected: widget.filter == category.key,
|
||||
onTap: () => widget.onTapCategory(category.key),
|
||||
options: widget.options,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.paddings.mainPadding.right - 4, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
for (var category in categories) ...[
|
||||
widget.options.categoriesOptions.categoryButtonBuilder?.call(
|
||||
category,
|
||||
() => widget.onTapCategory(category.key),
|
||||
widget.filter == category.key,
|
||||
widget.isOnTop,
|
||||
) ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: CategorySelectorButton(
|
||||
isOnTop: widget.isOnTop,
|
||||
category: category,
|
||||
selected: widget.filter == category.key,
|
||||
onTap: () => widget.onTapCategory(category.key),
|
||||
options: widget.options,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.paddings.mainPadding.right - 4, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ class CategorySelectorButton extends StatelessWidget {
|
|||
var theme = Theme.of(context);
|
||||
var size = MediaQuery.of(context).size;
|
||||
|
||||
return SizedBox(
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
height: isOnTop ? 140 : 40,
|
||||
child: TextButton(
|
||||
onPressed: onTap,
|
||||
|
@ -81,11 +82,13 @@ class CategorySelectorButton extends StatelessWidget {
|
|||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
category.icon,
|
||||
SizedBox(
|
||||
width:
|
||||
options.paddings.categoryButtonTextPadding ?? 8,
|
||||
),
|
||||
if (category.icon != null) ...[
|
||||
category.icon!,
|
||||
SizedBox(
|
||||
width:
|
||||
options.paddings.categoryButtonTextPadding ?? 8,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: _CategoryButtonText(
|
||||
category: category,
|
||||
|
@ -124,7 +127,9 @@ class _CategoryButtonText extends StatelessWidget {
|
|||
Widget build(BuildContext context) => Text(
|
||||
category.title,
|
||||
style: (options.theme.textStyles.categoryTitleStyle ??
|
||||
theme.textTheme.labelLarge)
|
||||
(selected
|
||||
? theme.textTheme.titleMedium
|
||||
: theme.textTheme.bodyMedium))
|
||||
?.copyWith(
|
||||
color: selected
|
||||
? options.theme.categorySelectionButtonSelectedTextColor ??
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class DefaultFilledButton extends StatelessWidget {
|
||||
const DefaultFilledButton({
|
||||
required this.onPressed,
|
||||
required this.buttonText,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Future<void> Function()? onPressed;
|
||||
final String buttonText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
return FilledButton(
|
||||
style: onPressed != null
|
||||
? ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onPressed: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
buttonText,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class PostCreationTextfield extends StatelessWidget {
|
||||
const PostCreationTextfield({
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
required this.validator,
|
||||
super.key,
|
||||
this.textMaxLength,
|
||||
this.decoration,
|
||||
this.textCapitalization,
|
||||
this.expands,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.fieldKey,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
final int? textMaxLength;
|
||||
final InputDecoration? decoration;
|
||||
final TextCapitalization? textCapitalization;
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
final bool? expands;
|
||||
final int? minLines;
|
||||
final int? maxLines;
|
||||
final String? Function(String?)? validator;
|
||||
final Key? fieldKey;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
return TextFormField(
|
||||
key: fieldKey,
|
||||
validator: validator,
|
||||
style: theme.textTheme.bodySmall,
|
||||
controller: controller,
|
||||
maxLength: textMaxLength,
|
||||
decoration: decoration ??
|
||||
InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 16,
|
||||
),
|
||||
hintText: hintText,
|
||||
hintStyle: theme.textTheme.bodySmall!.copyWith(
|
||||
color: theme.textTheme.bodySmall!.color!.withOpacity(0.5),
|
||||
),
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
textCapitalization: textCapitalization ?? TextCapitalization.none,
|
||||
expands: expands ?? false,
|
||||
minLines: minLines,
|
||||
maxLines: maxLines,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
||||
|
||||
|
@ -11,14 +12,12 @@ class ReactionBottom extends StatefulWidget {
|
|||
required this.onReactionSubmit,
|
||||
required this.messageInputBuilder,
|
||||
required this.translations,
|
||||
this.onPressSelectImage,
|
||||
this.iconColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Future<void> Function(String text) onReactionSubmit;
|
||||
final TextInputBuilder messageInputBuilder;
|
||||
final VoidCallback? onPressSelectImage;
|
||||
final TimelineTranslations translations;
|
||||
final Color? iconColor;
|
||||
|
||||
|
@ -30,56 +29,30 @@ class _ReactionBottomState extends State<ReactionBottom> {
|
|||
final TextEditingController _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
bottom: true,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
Widget build(BuildContext context) => Container(
|
||||
child: widget.messageInputBuilder(
|
||||
_textEditingController,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
height: 48,
|
||||
child: widget.messageInputBuilder(
|
||||
_textEditingController,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
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;
|
||||
if (value.isNotEmpty) {
|
||||
await widget.onReactionSubmit(value);
|
||||
_textEditingController.clear();
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
color: widget.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
var value = _textEditingController.text;
|
||||
if (value.isNotEmpty) {
|
||||
await widget.onReactionSubmit(value);
|
||||
_textEditingController.clear();
|
||||
}
|
||||
},
|
||||
icon: SvgPicture.asset(
|
||||
'assets/send.svg',
|
||||
package: 'flutter_timeline_view',
|
||||
// ignore: deprecated_member_use
|
||||
color: widget.iconColor,
|
||||
),
|
||||
widget.translations.writeComment,
|
||||
),
|
||||
),
|
||||
widget.translations.writeComment,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ class _HeartAnimationState extends State<HeartAnimation> {
|
|||
unawaited(
|
||||
Future.delayed(const Duration(milliseconds: 100)).then((value) async {
|
||||
active = widget.liked;
|
||||
// ignore: use_build_context_synchronously
|
||||
var navigator = Navigator.of(context);
|
||||
await Future.delayed(widget.duration);
|
||||
navigator.pop();
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
||||
|
||||
class TimelinePostWidget extends StatefulWidget {
|
||||
|
@ -52,324 +54,387 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
return InkWell(
|
||||
onTap: widget.onTap,
|
||||
child: SizedBox(
|
||||
height: widget.post.imageUrl != null || widget.post.image != null
|
||||
? widget.options.postWidgetHeight
|
||||
: null,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.post.creator != null)
|
||||
InkWell(
|
||||
onTap: widget.onUserTap != null
|
||||
? () =>
|
||||
widget.onUserTap?.call(widget.post.creator!.userId)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.post.creator!.imageUrl != null) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
widget.post.creator!.imageUrl!,
|
||||
),
|
||||
var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false;
|
||||
|
||||
return SizedBox(
|
||||
height: widget.post.imageUrl != null || widget.post.image != null
|
||||
? widget.options.postWidgetHeight
|
||||
: null,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.post.creator != null) ...[
|
||||
InkWell(
|
||||
onTap: widget.onUserTap != null
|
||||
? () =>
|
||||
widget.onUserTap?.call(widget.post.creator!.userId)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.post.creator!.imageUrl != null) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
widget.post.creator!.imageUrl!,
|
||||
),
|
||||
] else ...[
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
widget.options.nameBuilder
|
||||
?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles
|
||||
.postCreatorTitleStyle ??
|
||||
theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (widget.allowAllDeletion ||
|
||||
widget.post.creator?.userId == widget.userId)
|
||||
PopupMenuButton(
|
||||
onSelected: (value) async {
|
||||
if (value == 'delete') {
|
||||
await showPostDeletionConfirmationDialog(
|
||||
widget.options,
|
||||
context,
|
||||
widget.onPostDelete,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.deletePost,
|
||||
style: widget.options.theme.textStyles
|
||||
.deletePostStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
widget.options.theme.deleteIcon ??
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
widget.options.nameBuilder?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles
|
||||
.postCreatorTitleStyle ??
|
||||
theme.textTheme.titleSmall!.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: widget.options.theme.moreIcon ??
|
||||
Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (widget.allowAllDeletion ||
|
||||
widget.post.creator?.userId == widget.userId) ...[
|
||||
PopupMenuButton(
|
||||
onSelected: (value) async {
|
||||
if (value == 'delete') {
|
||||
await showPostDeletionConfirmationDialog(
|
||||
widget.options,
|
||||
context,
|
||||
widget.onPostDelete,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.options.translations.deletePost,
|
||||
style: widget
|
||||
.options.theme.textStyles.deletePostStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
widget.options.theme.deleteIcon ??
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: widget.options.theme.moreIcon ??
|
||||
Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
color: widget.options.theme.iconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
// image of the post
|
||||
if (widget.post.imageUrl != null || widget.post.image != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Flexible(
|
||||
flex: widget.options.postWidgetHeight != null ? 1 : 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: widget.options.doubleTapTolike
|
||||
? TappableImage(
|
||||
likeAndDislikeIcon:
|
||||
widget.options.likeAndDislikeIconsForDoubleTap,
|
||||
post: widget.post,
|
||||
userId: widget.userId,
|
||||
onLike: ({required bool liked}) async {
|
||||
var userId = widget.userId;
|
||||
|
||||
late TimelinePost result;
|
||||
|
||||
if (!liked) {
|
||||
result = await widget.service.postService.likePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
} else {
|
||||
result =
|
||||
await widget.service.postService.unlikePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
}
|
||||
|
||||
return result.likedBy?.contains(userId) ?? false;
|
||||
},
|
||||
)
|
||||
: widget.post.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
imageUrl: widget.post.imageUrl!,
|
||||
fit: BoxFit.fitWidth,
|
||||
)
|
||||
: Image.memory(
|
||||
width: double.infinity,
|
||||
widget.post.image!,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
// post information
|
||||
if (widget.options.iconsWithValues) ...[
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () async {
|
||||
var userId = widget.userId;
|
||||
|
||||
if (!isLikedByUser) {
|
||||
await widget.service.postService.likePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
} else {
|
||||
await widget.service.postService.unlikePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
isLikedByUser
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline_outlined,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text('${widget.post.likes}'),
|
||||
if (widget.post.reactionEnabled) ...[
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: widget.onTap,
|
||||
icon: widget.options.theme.commentIcon ??
|
||||
SvgPicture.asset(
|
||||
'assets/Comment.svg',
|
||||
package: 'flutter_timeline_view',
|
||||
// ignore: deprecated_member_use
|
||||
color: widget.options.theme.iconColor,
|
||||
width: widget.options.iconSize,
|
||||
height: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text('${widget.post.reaction}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
// image of the post
|
||||
if (widget.post.imageUrl != null || widget.post.image != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Flexible(
|
||||
flex: widget.options.postWidgetHeight != null ? 1 : 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: widget.options.doubleTapTolike
|
||||
? TappableImage(
|
||||
likeAndDislikeIcon:
|
||||
widget.options.likeAndDislikeIconsForDoubleTap,
|
||||
post: widget.post,
|
||||
userId: widget.userId,
|
||||
onLike: ({required bool liked}) async {
|
||||
var userId = widget.userId;
|
||||
|
||||
late TimelinePost result;
|
||||
|
||||
if (!liked) {
|
||||
result =
|
||||
await widget.service.postService.likePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
} else {
|
||||
result =
|
||||
await widget.service.postService.unlikePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
}
|
||||
|
||||
return result.likedBy?.contains(userId) ?? false;
|
||||
},
|
||||
)
|
||||
: widget.post.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
imageUrl: widget.post.imageUrl!,
|
||||
fit: BoxFit.fitWidth,
|
||||
)
|
||||
: Image.memory(
|
||||
width: double.infinity,
|
||||
widget.post.image!,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
] else ...[
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed:
|
||||
isLikedByUser ? widget.onTapUnlike : widget.onTapLike,
|
||||
icon: (isLikedByUser
|
||||
? widget.options.theme.likedIcon
|
||||
: widget.options.theme.likeIcon) ??
|
||||
Icon(
|
||||
isLikedByUser
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
// post information
|
||||
if (widget.options.iconsWithValues)
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
var userId = widget.userId;
|
||||
|
||||
var liked =
|
||||
widget.post.likedBy?.contains(userId) ?? false;
|
||||
|
||||
if (!liked) {
|
||||
await widget.service.postService.likePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
} else {
|
||||
await widget.service.postService.unlikePost(
|
||||
userId,
|
||||
widget.post,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
widget.post.likedBy?.contains(widget.userId) ?? false
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline_outlined,
|
||||
const SizedBox(width: 8),
|
||||
if (widget.post.reactionEnabled) ...[
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: widget.onTap,
|
||||
icon: widget.options.theme.commentIcon ??
|
||||
SvgPicture.asset(
|
||||
'assets/Comment.svg',
|
||||
package: 'flutter_timeline_view',
|
||||
// ignore: deprecated_member_use
|
||||
color: widget.options.theme.iconColor,
|
||||
width: widget.options.iconSize,
|
||||
height: widget.options.iconSize,
|
||||
),
|
||||
label: Text('${widget.post.likes}'),
|
||||
),
|
||||
if (widget.post.reactionEnabled)
|
||||
TextButton.icon(
|
||||
onPressed: widget.onTap,
|
||||
icon: widget.options.theme.commentIcon ??
|
||||
const Icon(
|
||||
Icons.chat_bubble_outline_outlined,
|
||||
),
|
||||
label: Text('${widget.post.reaction}'),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
if (widget.post.likedBy?.contains(widget.userId) ??
|
||||
false) ...[
|
||||
InkWell(
|
||||
onTap: widget.onTapUnlike,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
InkWell(
|
||||
onTap: widget.onTapLike,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
Icons.favorite_outline,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
if (widget.post.reactionEnabled) ...[
|
||||
Container(
|
||||
color: Colors.transparent,
|
||||
child: widget.options.theme.commentIcon ??
|
||||
Icon(
|
||||
Icons.chat_bubble_outline_rounded,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (widget.options.itemInfoBuilder != null) ...[
|
||||
widget.options.itemInfoBuilder!(
|
||||
post: widget.post,
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
'${widget.post.likes} '
|
||||
'${widget.options.translations.likesTitle}',
|
||||
style: widget
|
||||
.options.theme.textStyles.listPostLikeTitleAndAmount ??
|
||||
theme.textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: widget.options.nameBuilder?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles.listCreatorNameStyle ??
|
||||
theme.textTheme.titleSmall,
|
||||
children: [
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: widget.post.title,
|
||||
style:
|
||||
widget.options.theme.textStyles.listPostTitleStyle ??
|
||||
theme.textTheme.bodyMedium,
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
||||
if (widget.options.itemInfoBuilder != null) ...[
|
||||
widget.options.itemInfoBuilder!(
|
||||
post: widget.post,
|
||||
),
|
||||
] else ...[
|
||||
_PostLikeCountText(
|
||||
post: widget.post,
|
||||
options: widget.options,
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: widget.options.nameBuilder?.call(widget.post.creator) ??
|
||||
widget.post.creator?.fullName ??
|
||||
widget.options.translations.anonymousUser,
|
||||
style: widget.options.theme.textStyles.listCreatorNameStyle ??
|
||||
theme.textTheme.titleSmall!.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.post.title,
|
||||
style: widget.options.theme.textStyles.listPostTitleStyle ??
|
||||
theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
InkWell(
|
||||
onTap: widget.onTap,
|
||||
child: Text(
|
||||
widget.options.translations.viewPost,
|
||||
style: widget.options.theme.textStyles.viewPostStyle ??
|
||||
theme.textTheme.bodySmall,
|
||||
theme.textTheme.titleSmall!.copyWith(
|
||||
color: const Color(0xFF8D8D8D),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.options.dividerBuilder != null)
|
||||
widget.options.dividerBuilder!(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.options.dividerBuilder != null)
|
||||
widget.options.dividerBuilder!(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostLikeCountText extends StatelessWidget {
|
||||
const _PostLikeCountText({
|
||||
required this.post,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
final TimelineOptions options;
|
||||
final TimelinePost post;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var likeTranslation = post.likes > 1
|
||||
? options.translations.multipleLikesTitle
|
||||
: options.translations.oneLikeTitle;
|
||||
|
||||
return Text(
|
||||
'${post.likes} '
|
||||
'$likeTranslation',
|
||||
style: options.theme.textStyles.listPostLikeTitleAndAmount ??
|
||||
theme.textTheme.titleSmall!.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPostDeletionConfirmationDialog(
|
||||
TimelineOptions options,
|
||||
BuildContext context,
|
||||
Function() onPostDelete,
|
||||
) async {
|
||||
var theme = Theme.of(context);
|
||||
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,
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 64, vertical: 24),
|
||||
titlePadding: const EdgeInsets.only(left: 44, right: 44, top: 32),
|
||||
title: Text(
|
||||
options.translations.deleteConfirmationMessage,
|
||||
style: theme.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultFilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
buttonText: options.translations.deleteButton,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(
|
||||
options.translations.deleteCancelButton,
|
||||
style: theme.textTheme.bodyMedium!.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
color: theme.textTheme.bodyMedium?.color!.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
|
||||
name: flutter_timeline_view
|
||||
description: Visual elements of the Flutter Timeline Component
|
||||
version: 4.1.0
|
||||
|
||||
publish_to: none
|
||||
version: 5.1.0
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.3 <4.0.0'
|
||||
sdk: ">=3.1.3 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
@ -17,18 +16,14 @@ dependencies:
|
|||
intl: any
|
||||
cached_network_image: ^3.2.2
|
||||
dotted_border: ^2.1.0
|
||||
flutter_html: ^3.0.0-beta.2
|
||||
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 4.1.0
|
||||
flutter_image_picker:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||
ref: 1.0.5
|
||||
collection: any
|
||||
flutter_svg: ^2.0.10+1
|
||||
flutter_timeline_interface:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^5.1.0
|
||||
flutter_image_picker:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^4.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
@ -38,4 +33,5 @@ dev_dependencies:
|
|||
ref: 6.0.0
|
||||
|
||||
flutter:
|
||||
|
||||
assets:
|
||||
- assets/
|
||||
|
|
Loading…
Reference in a new issue