mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
Compare commits
No commits in common. "master" and "2.3.1" have entirely different histories.
60 changed files with 1602 additions and 2583 deletions
3
.fvmrc
3
.fvmrc
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"flutter": "3.22.2"
|
|
||||||
}
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -51,6 +51,3 @@ pubspec_overrides.yaml
|
||||||
**/example/windows
|
**/example/windows
|
||||||
**/example/web
|
**/example/web
|
||||||
**/example/README.md
|
**/example/README.md
|
||||||
|
|
||||||
# FVM Version Cache
|
|
||||||
.fvm/
|
|
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -1,50 +1,3 @@
|
||||||
## 5.1.1
|
|
||||||
|
|
||||||
- Be honest about which Dart and Flutter versions we support
|
|
||||||
- Relax our Firebase version constraint, we also support the current newest versions
|
|
||||||
|
|
||||||
## 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
|
|
||||||
- Show loading indicator when loading a post in the post screen
|
|
||||||
|
|
||||||
## 4.0.0
|
|
||||||
|
|
||||||
- Add a serviceBuilder to the userstory configuration
|
|
||||||
- Add a listHeaderBuilder for showing a header at the top of the list of posts in the timeline
|
|
||||||
- Add a getUserId function to retrieve the userId when needed in the userstory configuration
|
|
||||||
- Fix the timelinecategory selection by removing the categories with key null
|
|
||||||
- Set an optional max length on the default post title input field
|
|
||||||
- Add a postCreationFloatingActionButtonColor to the timeline theme to set the color of the floating action button
|
|
||||||
- Add a post and a category to the postViewOpenPageBuilder function
|
|
||||||
- Add a refresh functionality to the timeline with a pull to refresh callback to allow additional functionality when refreshing the timeline
|
|
||||||
- Use the adaptive variants of the material elements in the timeline
|
|
||||||
- Change the default blue color to the primary color of the Theme.of(context) in the timeline
|
|
||||||
- Change the TimelineTranslations constructor to require all translations or use the TimelineTranslations.empty constructor if you don't want to specify all translations
|
|
||||||
- Add a TimelinePaddingOptions class to store the padding options for the timeline
|
|
||||||
- fix the avatar size to match the new design
|
|
||||||
- Add the iconbutton for image uploading back to the ReactionBottom
|
|
||||||
- Fix category key is correctly used for saving timeline posts and category title is shown everywhere
|
|
||||||
- Fix when clicking on post delete in the post screen of the userstory it will now navigate back to the timeline and delete the post
|
|
||||||
- Fix like icon being used for both like and unliked posts
|
|
||||||
- Fix post creator can only like the post once and after it is actually created
|
|
||||||
- Change the CategorySelectorButton to use more styling options and allow for an icon to be shown
|
|
||||||
- Fix incorrect timeline reaction name
|
|
||||||
- Add a dialog for post deletion confirmation
|
|
||||||
- Add a callback method to determine if a user can delete posts that gets called when needed
|
|
||||||
|
|
||||||
## 3.0.1
|
|
||||||
|
|
||||||
- Fixed postOverviewScreen not displaying the creators name.
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
- Add default styling and default flow
|
|
||||||
|
|
||||||
## 2.3.1
|
## 2.3.1
|
||||||
|
|
||||||
- Updated readme.
|
- Updated readme.
|
||||||
|
|
40
README.md
40
README.md
|
@ -35,7 +35,45 @@ And import this package: import 'package:intl/date_symbol_data_local.dart';
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
To use the userstory add the following code somewhere in your widget tree:
|
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:
|
||||||
|
|
||||||
````
|
````
|
||||||
timeLineNavigatorUserStory(TimelineUserStoryConfiguration, context),
|
timeLineNavigatorUserStory(TimelineUserStoryConfiguration, context),
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../../CHANGELOG.md
|
|
|
@ -1 +0,0 @@
|
||||||
../../LICENSE
|
|
|
@ -1 +0,0 @@
|
||||||
../../README.md
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
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(
|
||||||
|
background: const Color(0XFFFAF9F6),
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ class NavigatorApp extends StatelessWidget {
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme:
|
colorScheme:
|
||||||
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
||||||
surface: const Color(0xFFB8E2E8),
|
background: const Color(0xFFB8E2E8),
|
||||||
),
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
|
|
|
@ -13,7 +13,7 @@ class WidgetApp extends StatelessWidget {
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme:
|
colorScheme:
|
||||||
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
||||||
surface: const Color(0xFFB8E2E8),
|
background: const Color(0xFFB8E2E8),
|
||||||
),
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,14 +7,31 @@ TimelineUserStoryConfiguration getConfig(TimelineService service) {
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
optionsBuilder: (context) => options,
|
optionsBuilder: (context) => options,
|
||||||
enablePostOverviewScreen: false,
|
enablePostOverviewScreen: false,
|
||||||
canDeleteAllPosts: (_) => true,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var options = TimelineOptions(
|
var options = TimelineOptions(
|
||||||
textInputBuilder: null,
|
textInputBuilder: null,
|
||||||
paddings: TimelinePaddingOptions(
|
padding: const EdgeInsets.all(20).copyWith(top: 28),
|
||||||
mainPadding: const EdgeInsets.all(20).copyWith(top: 28),
|
allowAllDeletion: true,
|
||||||
|
categoriesOptions: CategoriesOptions(
|
||||||
|
categoriesBuilder: (context) => [
|
||||||
|
const TimelineCategory(
|
||||||
|
key: null,
|
||||||
|
title: 'All',
|
||||||
|
icon: SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
const TimelineCategory(
|
||||||
|
key: 'category1',
|
||||||
|
title: 'Category 1',
|
||||||
|
icon: SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
const TimelineCategory(
|
||||||
|
key: 'category2',
|
||||||
|
title: 'Category 2',
|
||||||
|
icon: SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import 'package:example/apps/navigator/app.dart';
|
// 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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
initializeDateFormatting();
|
initializeDateFormatting();
|
||||||
|
|
||||||
runApp(const NavigatorApp());
|
// Uncomment any, but only one, of these lines to run the example with specific navigation.
|
||||||
|
|
||||||
|
// runApp(const WidgetApp());
|
||||||
|
// runApp(const NavigatorApp());
|
||||||
|
runApp(const GoRouterApp());
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ dependencies:
|
||||||
flutter_timeline:
|
flutter_timeline:
|
||||||
path: ../
|
path: ../
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
go_router: ^13.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
/// Flutter Timeline library
|
/// Flutter Timeline library
|
||||||
library flutter_timeline;
|
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/flutter_timeline_navigator_userstory.dart';
|
||||||
export 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
export 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
||||||
export 'package:flutter_timeline/src/routes.dart';
|
export 'package:flutter_timeline/src/routes.dart';
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
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 timelineScreen = TimelineScreen(
|
||||||
|
userId: config.userId,
|
||||||
|
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||||
|
service: config.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: Theme.of(context).primaryColor,
|
||||||
|
onPressed: () async => context.go(
|
||||||
|
TimelineUserStoryRoutes.timelinePostCreation,
|
||||||
|
),
|
||||||
|
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: Colors.black,
|
||||||
|
title: Text(
|
||||||
|
'Iconinstagram',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: timelineScreen,
|
||||||
|
floatingActionButton: button,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: TimelineUserStoryRoutes.timelineView,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
var post =
|
||||||
|
config.service.postService.getPost(state.pathParameters['post']!);
|
||||||
|
|
||||||
|
var timelinePostWidget = TimelinePostScreen(
|
||||||
|
userId: config.userId,
|
||||||
|
options: config.optionsBuilder(context),
|
||||||
|
service: config.service,
|
||||||
|
post: post!,
|
||||||
|
onPostDelete: () => config.onPostDelete?.call(context, post),
|
||||||
|
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) ??
|
||||||
|
Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: backButton,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
title: Text(
|
||||||
|
'Category',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: timelinePostWidget,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: TimelineUserStoryRoutes.timelinePostCreation,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
var timelinePostCreationWidget = TimelinePostCreationScreen(
|
||||||
|
userId: config.userId,
|
||||||
|
options: config.optionsBuilder(context),
|
||||||
|
service: config.service,
|
||||||
|
onPostCreated: (post) async {
|
||||||
|
var newPost = await config.service.postService.createPost(post);
|
||||||
|
if (context.mounted) {
|
||||||
|
if (config.afterPostCreationGoHome) {
|
||||||
|
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||||
|
} else {
|
||||||
|
await context
|
||||||
|
.push(TimelineUserStoryRoutes.timelineViewPath(newPost.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPostOverview: (post) async => context.push(
|
||||||
|
TimelineUserStoryRoutes.timelinePostOverview,
|
||||||
|
extra: post,
|
||||||
|
),
|
||||||
|
enablePostOverviewScreen: config.enablePostOverviewScreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
var backButton = IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios),
|
||||||
|
onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome),
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildScreenWithoutTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: config.postCreationOpenPageBuilder
|
||||||
|
?.call(context, timelinePostCreationWidget, backButton) ??
|
||||||
|
Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
title: Text(
|
||||||
|
config.optionsBuilder(context).translations.postCreation,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
leading: backButton,
|
||||||
|
),
|
||||||
|
body: timelinePostCreationWidget,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: TimelineUserStoryRoutes.timelinePostOverview,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
var post = state.extra! as TimelinePost;
|
||||||
|
|
||||||
|
var timelinePostOverviewWidget = TimelinePostOverviewScreen(
|
||||||
|
options: config.optionsBuilder(context),
|
||||||
|
service: config.service,
|
||||||
|
timelinePost: post,
|
||||||
|
onPostSubmit: (post) async {
|
||||||
|
await config.service.postService.createPost(post);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.go(TimelineUserStoryRoutes.timelineHome);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildScreenWithoutTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: config.postOverviewOpenPageBuilder?.call(
|
||||||
|
context,
|
||||||
|
timelinePostOverviewWidget,
|
||||||
|
) ??
|
||||||
|
timelinePostOverviewWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
|
@ -10,13 +10,11 @@ import 'package:flutter_timeline/flutter_timeline.dart';
|
||||||
/// This function creates a navigator for displaying user stories on a timeline.
|
/// This function creates a navigator for displaying user stories on a timeline.
|
||||||
/// It takes a [BuildContext] and an optional [TimelineUserStoryConfiguration]
|
/// It takes a [BuildContext] and an optional [TimelineUserStoryConfiguration]
|
||||||
/// as parameters. If no configuration is provided, default values will be used.
|
/// as parameters. If no configuration is provided, default values will be used.
|
||||||
late TimelineUserStoryConfiguration timelineUserStoryConfiguration;
|
|
||||||
|
|
||||||
Widget timeLineNavigatorUserStory({
|
Widget timeLineNavigatorUserStory({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
TimelineUserStoryConfiguration? configuration,
|
TimelineUserStoryConfiguration? configuration,
|
||||||
}) {
|
}) {
|
||||||
timelineUserStoryConfiguration = configuration ??
|
var config = configuration ??
|
||||||
TimelineUserStoryConfiguration(
|
TimelineUserStoryConfiguration(
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
service: TimelineService(
|
service: TimelineService(
|
||||||
|
@ -25,10 +23,7 @@ Widget timeLineNavigatorUserStory({
|
||||||
optionsBuilder: (context) => const TimelineOptions(),
|
optionsBuilder: (context) => const TimelineOptions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return _timelineScreenRoute(
|
return _timelineScreenRoute(configuration: config, context: context);
|
||||||
config: timelineUserStoryConfiguration,
|
|
||||||
context: context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget function that creates a timeline screen route.
|
/// A widget function that creates a timeline screen route.
|
||||||
|
@ -38,80 +33,52 @@ Widget timeLineNavigatorUserStory({
|
||||||
/// parameters. If no configuration is provided, default values will be used.
|
/// parameters. If no configuration is provided, default values will be used.
|
||||||
Widget _timelineScreenRoute({
|
Widget _timelineScreenRoute({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required TimelineUserStoryConfiguration config,
|
TimelineUserStoryConfiguration? configuration,
|
||||||
String? initalCategory,
|
|
||||||
}) {
|
}) {
|
||||||
var timelineScreen = TimelineScreen(
|
var config = configuration ??
|
||||||
timelineCategory: initalCategory,
|
TimelineUserStoryConfiguration(
|
||||||
userId: config.getUserId?.call(context) ?? config.userId,
|
userId: 'test_user',
|
||||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
service: TimelineService(
|
||||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
postService: LocalTimelinePostService(),
|
||||||
service: config.service,
|
),
|
||||||
options: config.optionsBuilder(context),
|
optionsBuilder: (context) => const TimelineOptions(),
|
||||||
onPostTap: (post) async =>
|
);
|
||||||
config.onPostTap?.call(context, post) ??
|
|
||||||
Navigator.of(context).push(
|
return Scaffold(
|
||||||
MaterialPageRoute(
|
appBar: AppBar(),
|
||||||
builder: (context) => _postDetailScreenRoute(
|
floatingActionButton: FloatingActionButton(
|
||||||
config: config,
|
onPressed: () async => Navigator.of(context).push(
|
||||||
context: context,
|
MaterialPageRoute(
|
||||||
post: post,
|
builder: (context) => _postCreationScreenRoute(
|
||||||
),
|
configuration: config,
|
||||||
|
context: context,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onRefresh: config.onRefresh,
|
),
|
||||||
filterEnabled: config.filterEnabled,
|
child: const Icon(Icons.add),
|
||||||
postWidgetBuilder: config.postWidgetBuilder,
|
),
|
||||||
);
|
body: TimelineScreen(
|
||||||
var theme = Theme.of(context);
|
service: config.service,
|
||||||
var button = FloatingActionButton(
|
options: config.optionsBuilder(context),
|
||||||
backgroundColor: config
|
userId: config.userId,
|
||||||
.optionsBuilder(context)
|
onPostTap: (post) async =>
|
||||||
.theme
|
config.onPostTap?.call(context, post) ??
|
||||||
.postCreationFloatingActionButtonColor ??
|
Navigator.of(context).push(
|
||||||
theme.colorScheme.primary,
|
MaterialPageRoute(
|
||||||
onPressed: () async {
|
builder: (context) => _postDetailScreenRoute(
|
||||||
var selectedCategory = config.service.postService.selectedCategory;
|
configuration: config,
|
||||||
if (selectedCategory != null && selectedCategory.key != null) {
|
context: context,
|
||||||
await Navigator.of(context).push(
|
post: post,
|
||||||
MaterialPageRoute(
|
),
|
||||||
builder: (context) => _postCreationScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
category: selectedCategory,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
onUserTap: (userId) {
|
||||||
} else {
|
config.onUserTap?.call(context, userId);
|
||||||
await Navigator.of(context).push(
|
},
|
||||||
MaterialPageRoute(
|
filterEnabled: config.filterEnabled,
|
||||||
builder: (context) => _postCategorySelectionScreen(
|
postWidgetBuilder: config.postWidgetBuilder,
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return config.homeOpenPageBuilder?.call(context, timelineScreen, button) ??
|
|
||||||
Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(
|
|
||||||
config.optionsBuilder(context).translations.timeLineScreenTitle,
|
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: timelineScreen,
|
|
||||||
floatingActionButton: button,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget function that creates a post detail screen route.
|
/// A widget function that creates a post detail screen route.
|
||||||
|
@ -123,50 +90,27 @@ Widget _timelineScreenRoute({
|
||||||
Widget _postDetailScreenRoute({
|
Widget _postDetailScreenRoute({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required TimelinePost post,
|
required TimelinePost post,
|
||||||
required TimelineUserStoryConfiguration config,
|
TimelineUserStoryConfiguration? configuration,
|
||||||
}) {
|
}) {
|
||||||
var timelinePostScreen = TimelinePostScreen(
|
var config = configuration ??
|
||||||
userId: config.getUserId?.call(context) ?? config.userId,
|
TimelineUserStoryConfiguration(
|
||||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
userId: 'test_user',
|
||||||
options: config.optionsBuilder(context),
|
service: TimelineService(
|
||||||
service: config.service,
|
postService: LocalTimelinePostService(),
|
||||||
post: post,
|
|
||||||
onPostDelete: () async =>
|
|
||||||
config.onPostDelete?.call(context, post) ??
|
|
||||||
() async {
|
|
||||||
await config.service.postService.deletePost(post);
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}.call(),
|
|
||||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
|
||||||
);
|
|
||||||
|
|
||||||
var category = config.service.postService.categories
|
|
||||||
.firstWhere((element) => element.key == post.category);
|
|
||||||
|
|
||||||
var backButton = IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: const Icon(Icons.arrow_back_ios),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return config.postViewOpenPageBuilder
|
|
||||||
?.call(context, timelinePostScreen, backButton, post, category) ??
|
|
||||||
Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
|
||||||
title: Text(
|
|
||||||
category.title.toLowerCase(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: timelinePostScreen,
|
optionsBuilder: (context) => const TimelineOptions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return TimelinePostScreen(
|
||||||
|
userId: config.userId,
|
||||||
|
service: config.service,
|
||||||
|
options: config.optionsBuilder(context),
|
||||||
|
post: post,
|
||||||
|
onPostDelete: () async {
|
||||||
|
config.onPostDelete?.call(context, post) ??
|
||||||
|
await config.service.postService.deletePost(post);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget function that creates a post creation screen route.
|
/// A widget function that creates a post creation screen route.
|
||||||
|
@ -176,79 +120,69 @@ Widget _postDetailScreenRoute({
|
||||||
/// as parameters. If no configuration is provided, default values will be used.
|
/// as parameters. If no configuration is provided, default values will be used.
|
||||||
Widget _postCreationScreenRoute({
|
Widget _postCreationScreenRoute({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required TimelineCategory category,
|
TimelineUserStoryConfiguration? configuration,
|
||||||
required TimelineUserStoryConfiguration config,
|
|
||||||
}) {
|
}) {
|
||||||
var timelinePostCreationScreen = TimelinePostCreationScreen(
|
var config = configuration ??
|
||||||
userId: config.getUserId?.call(context) ?? config.userId,
|
TimelineUserStoryConfiguration(
|
||||||
options: config.optionsBuilder(context),
|
userId: 'test_user',
|
||||||
service: config.service,
|
service: TimelineService(
|
||||||
onPostCreated: (post) async {
|
postService: LocalTimelinePostService(),
|
||||||
var newPost = await config.service.postService.createPost(post);
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
|
||||||
if (config.afterPostCreationGoHome) {
|
|
||||||
await Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _timelineScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
initalCategory: category.title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _postOverviewScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
post: newPost,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPostOverview: (post) async => Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _postOverviewScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
post: post,
|
|
||||||
),
|
),
|
||||||
|
optionsBuilder: (context) => const TimelineOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
config.optionsBuilder(context).translations.postCreation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
enablePostOverviewScreen: config.enablePostOverviewScreen,
|
body: TimelinePostCreationScreen(
|
||||||
postCategory: category.key,
|
userId: config.userId,
|
||||||
);
|
service: config.service,
|
||||||
|
options: config.optionsBuilder(context),
|
||||||
var backButton = IconButton(
|
onPostCreated: (post) async {
|
||||||
icon: const Icon(
|
await config.service.postService.createPost(post);
|
||||||
Icons.arrow_back_ios,
|
if (context.mounted) {
|
||||||
color: Colors.white,
|
if (config.afterPostCreationGoHome) {
|
||||||
),
|
await Navigator.pushReplacement(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
context,
|
||||||
);
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _timelineScreenRoute(
|
||||||
return config.postCreationOpenPageBuilder
|
configuration: config,
|
||||||
?.call(context, timelinePostCreationScreen, backButton) ??
|
context: context,
|
||||||
Scaffold(
|
),
|
||||||
appBar: AppBar(
|
),
|
||||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
);
|
||||||
leading: backButton,
|
} else {
|
||||||
title: Text(
|
await Navigator.pushReplacement(
|
||||||
config.optionsBuilder(context).translations.postCreation,
|
context,
|
||||||
style: TextStyle(
|
MaterialPageRoute(
|
||||||
color: Theme.of(context).primaryColor,
|
builder: (context) => _postDetailScreenRoute(
|
||||||
fontSize: 24,
|
configuration: config,
|
||||||
fontWeight: FontWeight.w800,
|
context: context,
|
||||||
|
post: post,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPostOverview: (post) async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _postOverviewScreenRoute(
|
||||||
|
configuration: config,
|
||||||
|
context: context,
|
||||||
|
post: post,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
body: timelinePostCreationScreen,
|
},
|
||||||
);
|
enablePostOverviewScreen: config.enablePostOverviewScreen,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget function that creates a post overview screen route.
|
/// A widget function that creates a post overview screen route.
|
||||||
|
@ -260,115 +194,32 @@ Widget _postCreationScreenRoute({
|
||||||
Widget _postOverviewScreenRoute({
|
Widget _postOverviewScreenRoute({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required TimelinePost post,
|
required TimelinePost post,
|
||||||
required TimelineUserStoryConfiguration config,
|
TimelineUserStoryConfiguration? configuration,
|
||||||
}) {
|
}) {
|
||||||
var timelinePostOverviewWidget = TimelinePostOverviewScreen(
|
var config = configuration ??
|
||||||
|
TimelineUserStoryConfiguration(
|
||||||
|
userId: 'test_user',
|
||||||
|
service: TimelineService(
|
||||||
|
postService: LocalTimelinePostService(),
|
||||||
|
),
|
||||||
|
optionsBuilder: (context) => const TimelineOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return TimelinePostOverviewScreen(
|
||||||
|
timelinePost: post,
|
||||||
options: config.optionsBuilder(context),
|
options: config.optionsBuilder(context),
|
||||||
service: config.service,
|
service: config.service,
|
||||||
timelinePost: post,
|
|
||||||
onPostSubmit: (post) async {
|
onPostSubmit: (post) async {
|
||||||
var createdPost = await config.service.postService.createPost(post);
|
await config.service.postService.createPost(post);
|
||||||
config.onPostCreate?.call(createdPost);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await Navigator.of(context).pushAndRemoveUntil(
|
await Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => _timelineScreenRoute(
|
builder: (context) =>
|
||||||
config: config,
|
_timelineScreenRoute(configuration: config, context: context),
|
||||||
context: context,
|
|
||||||
initalCategory: post.category,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(route) => false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
var backButton = IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
onPressed: () async => Navigator.of(context).pop(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return config.postOverviewOpenPageBuilder?.call(
|
|
||||||
context,
|
|
||||||
timelinePostOverviewWidget,
|
|
||||||
) ??
|
|
||||||
Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
|
||||||
leading: backButton,
|
|
||||||
title: Text(
|
|
||||||
config.optionsBuilder(context).translations.postCreation,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: timelinePostOverviewWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _postCategorySelectionScreen({
|
|
||||||
required BuildContext context,
|
|
||||||
required TimelineUserStoryConfiguration config,
|
|
||||||
}) {
|
|
||||||
var timelineSelectionScreen = TimelineSelectionScreen(
|
|
||||||
postService: config.service.postService,
|
|
||||||
options: config.optionsBuilder(context),
|
|
||||||
categories: config.service.postService.categories,
|
|
||||||
onCategorySelected: (category) async {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _postCreationScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
category: category,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
var backButton = IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: const Icon(Icons.arrow_back_ios),
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return config.categorySelectionOpenPageBuilder
|
|
||||||
?.call(context, timelineSelectionScreen) ??
|
|
||||||
Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
|
||||||
leading: backButton,
|
|
||||||
title: Text(
|
|
||||||
config.optionsBuilder(context).translations.postCreation,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
30
packages/flutter_timeline/lib/src/go_router.dart
Normal file
30
packages/flutter_timeline/lib/src/go_router.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// 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,
|
||||||
|
);
|
|
@ -48,9 +48,6 @@ class TimelineUserStoryConfiguration {
|
||||||
const TimelineUserStoryConfiguration({
|
const TimelineUserStoryConfiguration({
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.optionsBuilder,
|
required this.optionsBuilder,
|
||||||
this.getUserId,
|
|
||||||
this.serviceBuilder,
|
|
||||||
this.canDeleteAllPosts,
|
|
||||||
this.userId = 'test_user',
|
this.userId = 'test_user',
|
||||||
this.homeOpenPageBuilder,
|
this.homeOpenPageBuilder,
|
||||||
this.postCreationOpenPageBuilder,
|
this.postCreationOpenPageBuilder,
|
||||||
|
@ -58,32 +55,19 @@ class TimelineUserStoryConfiguration {
|
||||||
this.postOverviewOpenPageBuilder,
|
this.postOverviewOpenPageBuilder,
|
||||||
this.onPostTap,
|
this.onPostTap,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
this.onRefresh,
|
|
||||||
this.onPostDelete,
|
this.onPostDelete,
|
||||||
this.filterEnabled = false,
|
this.filterEnabled = false,
|
||||||
this.postWidgetBuilder,
|
this.postWidgetBuilder,
|
||||||
this.afterPostCreationGoHome = false,
|
this.afterPostCreationGoHome = false,
|
||||||
this.enablePostOverviewScreen = true,
|
this.enablePostOverviewScreen = false,
|
||||||
this.categorySelectionOpenPageBuilder,
|
|
||||||
this.onPostCreate,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The ID of the user associated with this user story configuration.
|
/// The ID of the user associated with this user story configuration.
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
/// A function to get the userId only when needed and with a context
|
|
||||||
final String Function(BuildContext context)? getUserId;
|
|
||||||
|
|
||||||
/// A function to determine if a user can delete posts that is called
|
|
||||||
/// when needed
|
|
||||||
final bool Function(BuildContext context)? canDeleteAllPosts;
|
|
||||||
|
|
||||||
/// The TimelineService responsible for fetching user story data.
|
/// The TimelineService responsible for fetching user story data.
|
||||||
final TimelineService service;
|
final TimelineService service;
|
||||||
|
|
||||||
/// A function to get the timeline service only when needed and with a context
|
|
||||||
final TimelineService Function(BuildContext context)? serviceBuilder;
|
|
||||||
|
|
||||||
/// A function that builds TimelineOptions based on the given BuildContext.
|
/// A function that builds TimelineOptions based on the given BuildContext.
|
||||||
final TimelineOptions Function(BuildContext context) optionsBuilder;
|
final TimelineOptions Function(BuildContext context) optionsBuilder;
|
||||||
|
|
||||||
|
@ -115,8 +99,6 @@ class TimelineUserStoryConfiguration {
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Widget child,
|
Widget child,
|
||||||
IconButton? button,
|
IconButton? button,
|
||||||
TimelinePost post,
|
|
||||||
TimelineCategory? category,
|
|
||||||
)? postViewOpenPageBuilder;
|
)? postViewOpenPageBuilder;
|
||||||
|
|
||||||
/// Open page builder function for the post overview page. This function
|
/// Open page builder function for the post overview page. This function
|
||||||
|
@ -134,9 +116,6 @@ class TimelineUserStoryConfiguration {
|
||||||
/// A callback function invoked when the user's profile is tapped.
|
/// A callback function invoked when the user's profile is tapped.
|
||||||
final Function(BuildContext context, String userId)? onUserTap;
|
final Function(BuildContext context, String userId)? onUserTap;
|
||||||
|
|
||||||
/// A callback function invoked when the timeline is refreshed by pulling down
|
|
||||||
final Function(BuildContext context, String? category)? onRefresh;
|
|
||||||
|
|
||||||
/// A callback function invoked when a post deletion is requested.
|
/// A callback function invoked when a post deletion is requested.
|
||||||
final Widget Function(BuildContext context, TimelinePost post)? onPostDelete;
|
final Widget Function(BuildContext context, TimelinePost post)? onPostDelete;
|
||||||
|
|
||||||
|
@ -153,13 +132,4 @@ class TimelineUserStoryConfiguration {
|
||||||
/// Boolean to enable redirect to home after post creation.
|
/// Boolean to enable redirect to home after post creation.
|
||||||
/// If false, it will redirect to created post screen
|
/// If false, it will redirect to created post screen
|
||||||
final bool afterPostCreationGoHome;
|
final bool afterPostCreationGoHome;
|
||||||
|
|
||||||
/// Open page builder function for the category selection page. This function
|
|
||||||
/// accepts a [BuildContext] and a child widget.
|
|
||||||
final Function(
|
|
||||||
BuildContext context,
|
|
||||||
Widget child,
|
|
||||||
)? categorySelectionOpenPageBuilder;
|
|
||||||
|
|
||||||
final Function(TimelinePost post)? onPostCreate;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,6 @@ mixin TimelineUserStoryRoutes {
|
||||||
static const String timelineHome = '/timeline';
|
static const String timelineHome = '/timeline';
|
||||||
static const String timelineView = '/timeline-view/:post';
|
static const String timelineView = '/timeline-view/:post';
|
||||||
static String timelineViewPath(String postId) => '/timeline-view/$postId';
|
static String timelineViewPath(String postId) => '/timeline-view/$postId';
|
||||||
static String timelinepostCreation(String category) =>
|
static const String timelinePostCreation = '/timeline-post-creation';
|
||||||
'/timeline-post-creation/$category';
|
|
||||||
|
|
||||||
static const String timelinePostCreation =
|
|
||||||
'/timeline-post-creation/:category';
|
|
||||||
static String timelinePostOverview = '/timeline-post-overview';
|
static String timelinePostOverview = '/timeline-post-overview';
|
||||||
static String timelineCategorySelection = '/timeline-category-selection';
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,27 +3,32 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
name: flutter_timeline
|
name: flutter_timeline
|
||||||
description: Visual elements and interface combined into one package
|
description: Visual elements and interface combined into one package
|
||||||
version: 5.1.1
|
version: 2.3.1
|
||||||
homepage: https://github.com/Iconica-Development/flutter_timeline
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.4.3 <4.0.0"
|
sdk: ">=3.1.3 <4.0.0"
|
||||||
flutter: '>=3.22.2'
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
go_router: any
|
||||||
|
|
||||||
flutter_timeline_view:
|
flutter_timeline_view:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
git:
|
||||||
version: ^5.1.1
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
|
path: packages/flutter_timeline_view
|
||||||
|
ref: 2.3.1
|
||||||
|
|
||||||
flutter_timeline_interface:
|
flutter_timeline_interface:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
git:
|
||||||
version: ^5.1.1
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
collection: ^1.18.0
|
path: packages/flutter_timeline_interface
|
||||||
|
ref: 2.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
flutter_iconica_analysis:
|
flutter_iconica_analysis:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../../CHANGELOG.md
|
|
|
@ -1 +0,0 @@
|
||||||
../../LICENSE
|
|
|
@ -1 +0,0 @@
|
||||||
../../README.md
|
|
|
@ -2,14 +2,17 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
class FirebaseTimelineOptions {
|
class FirebaseTimelineOptions {
|
||||||
const FirebaseTimelineOptions({
|
const FirebaseTimelineOptions({
|
||||||
this.usersCollectionName = 'users',
|
this.usersCollectionName = 'users',
|
||||||
this.timelineCollectionName = 'timeline',
|
this.timelineCollectionName = 'timeline',
|
||||||
this.timelineCategoryCollectionName = 'timeline_categories',
|
this.allTimelineCategories = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final String usersCollectionName;
|
final String usersCollectionName;
|
||||||
final String timelineCollectionName;
|
final String timelineCollectionName;
|
||||||
final String timelineCategoryCollectionName;
|
final List<String> allTimelineCategories;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
class FirebaseUserDocument {
|
class FirebaseUserDocument {
|
||||||
const FirebaseUserDocument({
|
const FirebaseUserDocument({
|
||||||
this.firstName,
|
this.firstName,
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -39,12 +38,6 @@ class FirebaseTimelinePostService
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> posts = [];
|
List<TimelinePost> posts = [];
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelineCategory> categories = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimelineCategory? selectedCategory;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||||
var postId = const Uuid().v4();
|
var postId = const Uuid().v4();
|
||||||
|
@ -114,10 +107,7 @@ class FirebaseTimelinePostService
|
||||||
updatedReactions.add(reaction.copyWith(creator: user));
|
updatedReactions.add(reaction.copyWith(creator: user));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var updatedPost = post.copyWith(
|
var updatedPost = post.copyWith(reactions: updatedReactions);
|
||||||
reactions: updatedReactions,
|
|
||||||
creator: await _userService.getUser(post.creatorId),
|
|
||||||
);
|
|
||||||
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
|
@ -125,6 +115,7 @@ class FirebaseTimelinePostService
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
||||||
|
debugPrint('fetching posts from firebase with category: $category');
|
||||||
var snapshot = (category != null)
|
var snapshot = (category != null)
|
||||||
? await _db
|
? await _db
|
||||||
.collection(_options.timelineCollectionName)
|
.collection(_options.timelineCollectionName)
|
||||||
|
@ -245,20 +236,10 @@ class FirebaseTimelinePostService
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost?> getPost(String postId) async {
|
TimelinePost? getPost(String postId) =>
|
||||||
var post = await _db
|
(posts.any((element) => element.id == postId))
|
||||||
.collection(_options.timelineCollectionName)
|
? posts.firstWhere((element) => element.id == postId)
|
||||||
.doc(postId)
|
: null;
|
||||||
.withConverter<TimelinePost>(
|
|
||||||
fromFirestore: (snapshot, _) => TimelinePost.fromJson(
|
|
||||||
snapshot.id,
|
|
||||||
snapshot.data()!,
|
|
||||||
),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
)
|
|
||||||
.get();
|
|
||||||
return post.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> getPosts(String? category) => posts
|
List<TimelinePost> getPosts(String? category) => posts
|
||||||
|
@ -270,7 +251,7 @@ class FirebaseTimelinePostService
|
||||||
// update the post with the new like
|
// update the post with the new like
|
||||||
var updatedPost = post.copyWith(
|
var updatedPost = post.copyWith(
|
||||||
likes: post.likes + 1,
|
likes: post.likes + 1,
|
||||||
likedBy: [...post.likedBy ?? [], userId],
|
likedBy: post.likedBy?..add(userId),
|
||||||
);
|
);
|
||||||
posts = posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
|
@ -374,122 +355,4 @@ class FirebaseTimelinePostService
|
||||||
|
|
||||||
return user;
|
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,30 +4,33 @@
|
||||||
|
|
||||||
name: flutter_timeline_firebase
|
name: flutter_timeline_firebase
|
||||||
description: Implementation of the Flutter Timeline interface for Firebase.
|
description: Implementation of the Flutter Timeline interface for Firebase.
|
||||||
version: 5.1.1
|
version: 2.3.1
|
||||||
homepage: https://github.com/Iconica-Development/flutter_timeline
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.4.3 <4.0.0"
|
sdk: '>=3.1.3 <4.0.0'
|
||||||
flutter: '>=3.22.2'
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cloud_firestore: '>=4.13.1 <6.0.0'
|
cloud_firestore: ^4.13.1
|
||||||
firebase_core: '>=2.22.0 <4.0.0'
|
firebase_core: ^2.22.0
|
||||||
firebase_storage: '>=11.5.1 <13.0.0'
|
firebase_storage: ^11.5.1
|
||||||
uuid: ^4.2.1
|
uuid: ^4.2.1
|
||||||
collection: ^1.18.0
|
|
||||||
flutter_timeline_interface:
|
flutter_timeline_interface:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
git:
|
||||||
version: ^5.1.1
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
|
path: packages/flutter_timeline_interface
|
||||||
|
ref: 2.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
flutter_iconica_analysis:
|
flutter_iconica_analysis:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
ref: 6.0.0
|
ref: 6.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../../CHANGELOG.md
|
|
|
@ -1 +0,0 @@
|
||||||
../../LICENSE
|
|
|
@ -1 +0,0 @@
|
||||||
../../README.md
|
|
|
@ -5,44 +5,13 @@ class TimelineCategory {
|
||||||
const TimelineCategory({
|
const TimelineCategory({
|
||||||
required this.key,
|
required this.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.icon,
|
required this.icon,
|
||||||
this.canCreate = true,
|
this.canCreate = true,
|
||||||
this.canView = 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? key;
|
||||||
final String title;
|
final String title;
|
||||||
final Widget? icon;
|
final Widget icon;
|
||||||
final bool canCreate;
|
final bool canCreate;
|
||||||
final bool canView;
|
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,28 +13,11 @@ class TimelinePosterUserModel {
|
||||||
this.imageUrl,
|
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 userId;
|
||||||
final String? firstName;
|
final String? firstName;
|
||||||
final String? lastName;
|
final String? lastName;
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'first_name': firstName,
|
|
||||||
'last_name': lastName,
|
|
||||||
'image_url': imageUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
String? get fullName {
|
String? get fullName {
|
||||||
var fullName = '';
|
var fullName = '';
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ class TimelinePostReaction {
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
this.creator,
|
this.creator,
|
||||||
this.createdAtString,
|
this.createdAtString,
|
||||||
this.likedBy,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory TimelinePostReaction.fromJson(
|
factory TimelinePostReaction.fromJson(
|
||||||
|
@ -32,7 +31,6 @@ class TimelinePostReaction {
|
||||||
imageUrl: json['image_url'] as String?,
|
imageUrl: json['image_url'] as String?,
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
createdAtString: 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.
|
/// The unique identifier of the reaction.
|
||||||
|
@ -59,8 +57,6 @@ class TimelinePostReaction {
|
||||||
/// Reaction creation date as String with microseconds.
|
/// Reaction creation date as String with microseconds.
|
||||||
final String? createdAtString;
|
final String? createdAtString;
|
||||||
|
|
||||||
final List<String>? likedBy;
|
|
||||||
|
|
||||||
TimelinePostReaction copyWith({
|
TimelinePostReaction copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? postId,
|
String? postId,
|
||||||
|
@ -69,7 +65,6 @@ class TimelinePostReaction {
|
||||||
String? reaction,
|
String? reaction,
|
||||||
String? imageUrl,
|
String? imageUrl,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
List<String>? likedBy,
|
|
||||||
}) =>
|
}) =>
|
||||||
TimelinePostReaction(
|
TimelinePostReaction(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
@ -79,7 +74,6 @@ class TimelinePostReaction {
|
||||||
reaction: reaction ?? this.reaction,
|
reaction: reaction ?? this.reaction,
|
||||||
imageUrl: imageUrl ?? this.imageUrl,
|
imageUrl: imageUrl ?? this.imageUrl,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
likedBy: likedBy ?? this.likedBy,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||||
|
@ -88,7 +82,6 @@ class TimelinePostReaction {
|
||||||
'reaction': reaction,
|
'reaction': reaction,
|
||||||
'image_url': imageUrl,
|
'image_url': imageUrl,
|
||||||
'created_at': createdAt.toIso8601String(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
'liked_by': likedBy,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,7 +91,6 @@ class TimelinePostReaction {
|
||||||
'reaction': reaction,
|
'reaction': reaction,
|
||||||
'image_url': imageUrl,
|
'image_url': imageUrl,
|
||||||
'created_at': createdAtString,
|
'created_at': createdAtString,
|
||||||
'liked_by': likedBy,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,11 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
|
||||||
|
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
||||||
|
|
||||||
abstract class TimelinePostService with ChangeNotifier {
|
abstract class TimelinePostService with ChangeNotifier {
|
||||||
List<TimelinePost> posts = [];
|
List<TimelinePost> posts = [];
|
||||||
List<TimelineCategory> categories = [];
|
|
||||||
TimelineCategory? selectedCategory;
|
|
||||||
|
|
||||||
Future<void> deletePost(TimelinePost post);
|
Future<void> deletePost(TimelinePost post);
|
||||||
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
||||||
|
@ -18,7 +17,7 @@ abstract class TimelinePostService with ChangeNotifier {
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category);
|
Future<List<TimelinePost>> fetchPosts(String? category);
|
||||||
Future<TimelinePost> fetchPost(TimelinePost post);
|
Future<TimelinePost> fetchPost(TimelinePost post);
|
||||||
Future<List<TimelinePost>> fetchPostsPaginated(String? category, int limit);
|
Future<List<TimelinePost>> fetchPostsPaginated(String? category, int limit);
|
||||||
Future<TimelinePost?> getPost(String postId);
|
TimelinePost? getPost(String postId);
|
||||||
List<TimelinePost> getPosts(String? category);
|
List<TimelinePost> getPosts(String? category);
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category);
|
Future<List<TimelinePost>> refreshPosts(String? category);
|
||||||
Future<TimelinePost> fetchPostDetails(TimelinePost post);
|
Future<TimelinePost> fetchPostDetails(TimelinePost post);
|
||||||
|
@ -29,17 +28,4 @@ abstract class TimelinePostService with ChangeNotifier {
|
||||||
});
|
});
|
||||||
Future<TimelinePost> likePost(String userId, TimelinePost post);
|
Future<TimelinePost> likePost(String userId, TimelinePost post);
|
||||||
Future<TimelinePost> unlikePost(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,18 +4,19 @@
|
||||||
|
|
||||||
name: flutter_timeline_interface
|
name: flutter_timeline_interface
|
||||||
description: Interface for the service of the Flutter Timeline component
|
description: Interface for the service of the Flutter Timeline component
|
||||||
version: 5.1.1
|
version: 2.3.1
|
||||||
homepage: https://github.com/Iconica-Development/flutter_timeline
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.4.3 <4.0.0'
|
sdk: '>=3.1.3 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
flutter_iconica_analysis:
|
flutter_iconica_analysis:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../../CHANGELOG.md
|
|
|
@ -1 +0,0 @@
|
||||||
../../LICENSE
|
|
|
@ -1 +0,0 @@
|
||||||
../../README.md
|
|
|
@ -1,3 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 713 B |
|
@ -1,3 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -5,7 +5,6 @@
|
||||||
library flutter_timeline_view;
|
library flutter_timeline_view;
|
||||||
|
|
||||||
export 'src/config/timeline_options.dart';
|
export 'src/config/timeline_options.dart';
|
||||||
export 'src/config/timeline_paddings.dart';
|
|
||||||
export 'src/config/timeline_styles.dart';
|
export 'src/config/timeline_styles.dart';
|
||||||
export 'src/config/timeline_theme.dart';
|
export 'src/config/timeline_theme.dart';
|
||||||
export 'src/config/timeline_translations.dart';
|
export 'src/config/timeline_translations.dart';
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_paddings.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_theme.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_theme.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -14,12 +13,12 @@ class TimelineOptions {
|
||||||
const TimelineOptions({
|
const TimelineOptions({
|
||||||
this.theme = const TimelineTheme(),
|
this.theme = const TimelineTheme(),
|
||||||
this.translations = const TimelineTranslations.empty(),
|
this.translations = const TimelineTranslations.empty(),
|
||||||
this.paddings = const TimelinePaddingOptions(),
|
|
||||||
this.imagePickerConfig = const ImagePickerConfig(),
|
this.imagePickerConfig = const ImagePickerConfig(),
|
||||||
this.imagePickerTheme,
|
this.imagePickerTheme = const ImagePickerTheme(),
|
||||||
this.timelinePostHeight,
|
this.timelinePostHeight,
|
||||||
|
this.allowAllDeletion = false,
|
||||||
this.sortCommentsAscending = true,
|
this.sortCommentsAscending = true,
|
||||||
this.sortPostsAscending = false,
|
this.sortPostsAscending,
|
||||||
this.doubleTapTolike = false,
|
this.doubleTapTolike = false,
|
||||||
this.iconsWithValues = false,
|
this.iconsWithValues = false,
|
||||||
this.likeAndDislikeIconsForDoubleTap = const (
|
this.likeAndDislikeIconsForDoubleTap = const (
|
||||||
|
@ -38,8 +37,11 @@ class TimelineOptions {
|
||||||
this.userAvatarBuilder,
|
this.userAvatarBuilder,
|
||||||
this.anonymousAvatarBuilder,
|
this.anonymousAvatarBuilder,
|
||||||
this.nameBuilder,
|
this.nameBuilder,
|
||||||
this.iconSize = 24,
|
this.padding = const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
this.iconSize = 26,
|
||||||
this.postWidgetHeight,
|
this.postWidgetHeight,
|
||||||
|
this.postPadding =
|
||||||
|
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
|
||||||
this.filterOptions = const FilterOptions(),
|
this.filterOptions = const FilterOptions(),
|
||||||
this.categoriesOptions = const CategoriesOptions(),
|
this.categoriesOptions = const CategoriesOptions(),
|
||||||
this.requireImageForPost = false,
|
this.requireImageForPost = false,
|
||||||
|
@ -47,12 +49,6 @@ class TimelineOptions {
|
||||||
this.maxTitleLength,
|
this.maxTitleLength,
|
||||||
this.minContentLength,
|
this.minContentLength,
|
||||||
this.maxContentLength,
|
this.maxContentLength,
|
||||||
this.categorySelectorButtonBuilder,
|
|
||||||
this.postOverviewButtonBuilder,
|
|
||||||
this.deletionDialogBuilder,
|
|
||||||
this.listHeaderBuilder,
|
|
||||||
this.titleInputDecoration,
|
|
||||||
this.contentInputDecoration,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Theming options for the timeline
|
/// Theming options for the timeline
|
||||||
|
@ -70,15 +66,15 @@ class TimelineOptions {
|
||||||
/// Whether to sort posts ascending or descending
|
/// Whether to sort posts ascending or descending
|
||||||
final bool? sortPostsAscending;
|
final bool? sortPostsAscending;
|
||||||
|
|
||||||
|
/// Allow all posts to be deleted instead of
|
||||||
|
/// only the posts of the current user
|
||||||
|
final bool allowAllDeletion;
|
||||||
|
|
||||||
/// The height of a post in the timeline
|
/// The height of a post in the timeline
|
||||||
final double? timelinePostHeight;
|
final double? timelinePostHeight;
|
||||||
|
|
||||||
/// Class that contains all the translations used in the timeline
|
|
||||||
final TimelineTranslations translations;
|
final TimelineTranslations translations;
|
||||||
|
|
||||||
/// Class that contains all the paddings used in the timeline
|
|
||||||
final TimelinePaddingOptions paddings;
|
|
||||||
|
|
||||||
final ButtonBuilder? buttonBuilder;
|
final ButtonBuilder? buttonBuilder;
|
||||||
|
|
||||||
final TextInputBuilder? textInputBuilder;
|
final TextInputBuilder? textInputBuilder;
|
||||||
|
@ -93,7 +89,7 @@ class TimelineOptions {
|
||||||
|
|
||||||
/// ImagePickerTheme can be used to change the UI of the
|
/// ImagePickerTheme can be used to change the UI of the
|
||||||
/// Image Picker Widget to change the text/icons to your liking.
|
/// Image Picker Widget to change the text/icons to your liking.
|
||||||
final ImagePickerTheme? imagePickerTheme;
|
final ImagePickerTheme imagePickerTheme;
|
||||||
|
|
||||||
/// ImagePickerConfig can be used to define the
|
/// ImagePickerConfig can be used to define the
|
||||||
/// size and quality for the uploaded image.
|
/// size and quality for the uploaded image.
|
||||||
|
@ -114,12 +110,18 @@ class TimelineOptions {
|
||||||
/// The builder for the divider
|
/// The builder for the divider
|
||||||
final Widget Function()? dividerBuilder;
|
final Widget Function()? dividerBuilder;
|
||||||
|
|
||||||
|
/// The padding between posts in the timeline
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
/// Size of icons like the comment and like icons. Dafualts to 26
|
/// Size of icons like the comment and like icons. Dafualts to 26
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
|
|
||||||
/// Sets a predefined height for the postWidget.
|
/// Sets a predefined height for the postWidget.
|
||||||
final double? postWidgetHeight;
|
final double? postWidgetHeight;
|
||||||
|
|
||||||
|
/// Padding of each post
|
||||||
|
final EdgeInsets postPadding;
|
||||||
|
|
||||||
/// Options for filtering
|
/// Options for filtering
|
||||||
final FilterOptions filterOptions;
|
final FilterOptions filterOptions;
|
||||||
|
|
||||||
|
@ -140,68 +142,43 @@ class TimelineOptions {
|
||||||
|
|
||||||
/// Maximum length of the post content
|
/// Maximum length of the post content
|
||||||
final int? maxContentLength;
|
final int? maxContentLength;
|
||||||
|
|
||||||
/// Builder for the category selector button
|
|
||||||
/// on the timeline category selection screen
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
Function() onPressed,
|
|
||||||
String text,
|
|
||||||
)? categorySelectorButtonBuilder;
|
|
||||||
|
|
||||||
/// This widgetbuilder is placed at the top of the list of posts and can be
|
|
||||||
/// used to add custom elements
|
|
||||||
final Widget Function(BuildContext context, String? category)?
|
|
||||||
listHeaderBuilder;
|
|
||||||
|
|
||||||
/// Builder for the post overview button
|
|
||||||
/// on the timeline post overview screen
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
Function() onPressed,
|
|
||||||
String text,
|
|
||||||
TimelinePost post,
|
|
||||||
)? postOverviewButtonBuilder;
|
|
||||||
|
|
||||||
/// Optional builder to override the default alertdialog for post deletion
|
|
||||||
/// It should pop the navigator with true to delete the post and
|
|
||||||
/// false to cancel deletion
|
|
||||||
final WidgetBuilder? deletionDialogBuilder;
|
|
||||||
|
|
||||||
/// inputdecoration for the title textfield
|
|
||||||
final InputDecoration? titleInputDecoration;
|
|
||||||
|
|
||||||
/// inputdecoration for the content textfield
|
|
||||||
final InputDecoration? contentInputDecoration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoriesOptions {
|
class CategoriesOptions {
|
||||||
const CategoriesOptions({
|
const CategoriesOptions({
|
||||||
|
this.categoriesBuilder,
|
||||||
this.categoryButtonBuilder,
|
this.categoryButtonBuilder,
|
||||||
this.categorySelectorHorizontalPadding,
|
this.categorySelectorHorizontalPadding,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// List of categories that the user can select.
|
/// List of categories that the user can select.
|
||||||
/// If this is null no categories will be shown.
|
/// If this is null no categories will be shown.
|
||||||
|
final List<TimelineCategory> Function(BuildContext context)?
|
||||||
|
categoriesBuilder;
|
||||||
|
|
||||||
/// Abilty to override the standard category selector
|
/// Abilty to override the standard category selector
|
||||||
final Widget Function(
|
final Widget Function({
|
||||||
TimelineCategory category,
|
required String? categoryKey,
|
||||||
Function() onTap,
|
required String categoryName,
|
||||||
// ignore: avoid_positional_boolean_parameters
|
required Function onTap,
|
||||||
bool selected,
|
required bool selected,
|
||||||
bool isOnTop,
|
})? categoryButtonBuilder;
|
||||||
)? categoryButtonBuilder;
|
|
||||||
|
|
||||||
/// Overides the standard horizontal padding of the whole category selector.
|
/// Overides the standard horizontal padding of the whole category selector.
|
||||||
final double? categorySelectorHorizontalPadding;
|
final double? categorySelectorHorizontalPadding;
|
||||||
|
|
||||||
TimelineCategory? getCategoryByKey(
|
TimelineCategory? getCategoryByKey(
|
||||||
List<TimelineCategory> categories,
|
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String? key,
|
String? key,
|
||||||
) =>
|
) {
|
||||||
categories.firstWhereOrNull((category) => category.key == key);
|
if (categoriesBuilder == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoriesBuilder!
|
||||||
|
.call(context)
|
||||||
|
.firstWhereOrNull((category) => category.key == key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterOptions {
|
class FilterOptions {
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// This class contains the paddings used in the timeline options
|
|
||||||
class TimelinePaddingOptions {
|
|
||||||
const TimelinePaddingOptions({
|
|
||||||
this.mainPadding =
|
|
||||||
const EdgeInsets.only(left: 32, top: 20, right: 32, bottom: 40),
|
|
||||||
this.postPadding =
|
|
||||||
const EdgeInsets.only(left: 12.0, top: 12, right: 12.0, bottom: 8),
|
|
||||||
this.postOverviewButtonBottomPadding = 30.0,
|
|
||||||
this.categoryButtonTextPadding,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The padding between posts in the timeline
|
|
||||||
final EdgeInsets mainPadding;
|
|
||||||
|
|
||||||
/// The padding of each post
|
|
||||||
final EdgeInsets postPadding;
|
|
||||||
|
|
||||||
/// The bottom padding of the button on the post overview screen
|
|
||||||
final double postOverviewButtonBottomPadding;
|
|
||||||
|
|
||||||
/// The padding between the icon and the text in the category button
|
|
||||||
final double? categoryButtonTextPadding;
|
|
||||||
}
|
|
|
@ -15,11 +15,6 @@ class TimelineTheme {
|
||||||
this.sendIcon,
|
this.sendIcon,
|
||||||
this.moreIcon,
|
this.moreIcon,
|
||||||
this.deleteIcon,
|
this.deleteIcon,
|
||||||
this.categorySelectionButtonBorderColor,
|
|
||||||
this.categorySelectionButtonBackgroundColor,
|
|
||||||
this.categorySelectionButtonSelectedTextColor,
|
|
||||||
this.categorySelectionButtonUnselectedTextColor,
|
|
||||||
this.postCreationFloatingActionButtonColor,
|
|
||||||
this.textStyles = const TimelineTextStyles(),
|
this.textStyles = const TimelineTextStyles(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,23 +38,5 @@ class TimelineTheme {
|
||||||
/// The icon for delete action (delete post)
|
/// The icon for delete action (delete post)
|
||||||
final Widget? deleteIcon;
|
final Widget? deleteIcon;
|
||||||
|
|
||||||
/// The text style overrides for all the texts in the timeline
|
|
||||||
final TimelineTextStyles textStyles;
|
final TimelineTextStyles textStyles;
|
||||||
|
|
||||||
/// The color of the border around the category in the selection screen
|
|
||||||
final Color? categorySelectionButtonBorderColor;
|
|
||||||
|
|
||||||
/// The color of the background of the category selection button in the
|
|
||||||
/// selection screen
|
|
||||||
final Color? categorySelectionButtonBackgroundColor;
|
|
||||||
|
|
||||||
/// The color of the text of the category selection button when it is selected
|
|
||||||
final Color? categorySelectionButtonSelectedTextColor;
|
|
||||||
|
|
||||||
/// The color of the text of the category selection button when
|
|
||||||
/// it is not selected
|
|
||||||
final Color? categorySelectionButtonUnselectedTextColor;
|
|
||||||
|
|
||||||
/// The color of the floating action button on the overview screen
|
|
||||||
final Color? postCreationFloatingActionButtonColor;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
|
||||||
/// Class that holds all the translations for the timeline component view and
|
|
||||||
/// the corresponding userstory
|
|
||||||
class TimelineTranslations {
|
class TimelineTranslations {
|
||||||
/// TimelineTranslations constructor where everything is required use this
|
|
||||||
/// if you want to be sure to have all translations specified
|
|
||||||
/// If you just want the default values use the empty constructor
|
|
||||||
/// and optionally override the values with the copyWith method
|
|
||||||
const TimelineTranslations({
|
const TimelineTranslations({
|
||||||
required this.anonymousUser,
|
required this.anonymousUser,
|
||||||
required this.noPosts,
|
required this.noPosts,
|
||||||
|
@ -30,16 +23,12 @@ class TimelineTranslations {
|
||||||
required this.checkPost,
|
required this.checkPost,
|
||||||
required this.deletePost,
|
required this.deletePost,
|
||||||
required this.deleteReaction,
|
required this.deleteReaction,
|
||||||
required this.deleteConfirmationMessage,
|
|
||||||
required this.deleteConfirmationTitle,
|
|
||||||
required this.deleteCancelButton,
|
|
||||||
required this.deleteButton,
|
|
||||||
required this.viewPost,
|
required this.viewPost,
|
||||||
required this.oneLikeTitle,
|
required this.likesTitle,
|
||||||
required this.multipleLikesTitle,
|
|
||||||
required this.commentsTitle,
|
required this.commentsTitle,
|
||||||
required this.firstComment,
|
required this.firstComment,
|
||||||
required this.writeComment,
|
required this.writeComment,
|
||||||
|
required this.postAt,
|
||||||
required this.postLoadingError,
|
required this.postLoadingError,
|
||||||
required this.timelineSelectionDescription,
|
required this.timelineSelectionDescription,
|
||||||
required this.searchHint,
|
required this.searchHint,
|
||||||
|
@ -48,65 +37,40 @@ class TimelineTranslations {
|
||||||
required this.postCreation,
|
required this.postCreation,
|
||||||
required this.yes,
|
required this.yes,
|
||||||
required this.no,
|
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
|
const TimelineTranslations.empty()
|
||||||
const TimelineTranslations.empty({
|
: anonymousUser = 'Anonymous user',
|
||||||
this.anonymousUser = 'Anonymous user',
|
noPosts = 'No posts yet',
|
||||||
this.noPosts = 'No posts yet',
|
noPostsWithFilter = 'No posts with this filter',
|
||||||
this.noPostsWithFilter = 'No posts with this filter',
|
title = 'Title',
|
||||||
this.title = 'Title',
|
content = 'Content',
|
||||||
this.titleHintText = 'Title...',
|
contentDescription = 'What do you want to share?',
|
||||||
this.content = 'Content',
|
uploadImage = 'Upload image',
|
||||||
this.contentHintText = 'Content...',
|
uploadImageDescription = 'Upload an image to your message (optional)',
|
||||||
this.contentDescription = 'What do you want to share?',
|
allowComments = 'Are people allowed to comment?',
|
||||||
this.uploadImage = 'Upload image',
|
allowCommentsDescription =
|
||||||
this.uploadImageDescription = 'Upload an image to your message (optional)',
|
'Indicate whether people are allowed to respond',
|
||||||
this.allowComments = 'Are people allowed to comment?',
|
commentsTitleOnPost = 'Comments',
|
||||||
this.allowCommentsDescription =
|
checkPost = 'Check post overview',
|
||||||
'Indicate whether people are allowed to respond',
|
deletePost = 'Delete post',
|
||||||
this.commentsTitleOnPost = 'Comments',
|
deleteReaction = 'Delete Reaction',
|
||||||
this.checkPost = 'Overview',
|
viewPost = 'View post',
|
||||||
this.deletePost = 'Delete post',
|
likesTitle = 'Likes',
|
||||||
this.deleteConfirmationTitle = 'Delete Post',
|
commentsTitle = 'Are people allowed to comment?',
|
||||||
this.deleteConfirmationMessage =
|
firstComment = 'Be the first to comment',
|
||||||
'Are you sure you want to delete this post?',
|
writeComment = 'Write your comment here...',
|
||||||
this.deleteButton = 'Delete',
|
postAt = 'at',
|
||||||
this.deleteCancelButton = 'Cancel',
|
postLoadingError = 'Something went wrong while loading the post',
|
||||||
this.deleteReaction = 'Delete Reaction',
|
timelineSelectionDescription = 'Choose a category',
|
||||||
this.viewPost = 'View post',
|
searchHint = 'Search...',
|
||||||
this.oneLikeTitle = 'like',
|
postOverview = 'Post Overview',
|
||||||
this.multipleLikesTitle = 'likes',
|
postIn = 'Post in',
|
||||||
this.commentsTitle = 'Are people allowed to comment?',
|
postCreation = 'Create Post',
|
||||||
this.firstComment = 'Be the first to comment',
|
titleHintText = 'Title...',
|
||||||
this.writeComment = 'Write your comment here...',
|
contentHintText = 'Context...',
|
||||||
this.postLoadingError = 'Something went wrong while loading the post',
|
yes = 'Yes',
|
||||||
this.timelineSelectionDescription = 'Choose a category',
|
no = 'No';
|
||||||
this.searchHint = 'Search...',
|
|
||||||
this.postOverview = 'Post Overview',
|
|
||||||
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;
|
final String noPosts;
|
||||||
final String noPostsWithFilter;
|
final String noPostsWithFilter;
|
||||||
|
@ -120,22 +84,15 @@ class TimelineTranslations {
|
||||||
final String allowComments;
|
final String allowComments;
|
||||||
final String allowCommentsDescription;
|
final String allowCommentsDescription;
|
||||||
final String checkPost;
|
final String checkPost;
|
||||||
|
final String postAt;
|
||||||
|
|
||||||
final String titleHintText;
|
final String titleHintText;
|
||||||
final String contentHintText;
|
final String contentHintText;
|
||||||
final String titleErrorText;
|
|
||||||
final String contentErrorText;
|
|
||||||
|
|
||||||
final String deletePost;
|
final String deletePost;
|
||||||
final String deleteConfirmationTitle;
|
|
||||||
final String deleteConfirmationMessage;
|
|
||||||
final String deleteButton;
|
|
||||||
final String deleteCancelButton;
|
|
||||||
|
|
||||||
final String deleteReaction;
|
final String deleteReaction;
|
||||||
final String viewPost;
|
final String viewPost;
|
||||||
final String oneLikeTitle;
|
final String likesTitle;
|
||||||
final String multipleLikesTitle;
|
|
||||||
final String commentsTitle;
|
final String commentsTitle;
|
||||||
final String commentsTitleOnPost;
|
final String commentsTitleOnPost;
|
||||||
final String writeComment;
|
final String writeComment;
|
||||||
|
@ -150,18 +107,9 @@ class TimelineTranslations {
|
||||||
final String postIn;
|
final String postIn;
|
||||||
final String postCreation;
|
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 yes;
|
||||||
final String no;
|
final String no;
|
||||||
final String timeLineScreenTitle;
|
|
||||||
|
|
||||||
/// Method to override the default values of the translations
|
|
||||||
TimelineTranslations copyWith({
|
TimelineTranslations copyWith({
|
||||||
String? noPosts,
|
String? noPosts,
|
||||||
String? noPostsWithFilter,
|
String? noPostsWithFilter,
|
||||||
|
@ -175,15 +123,11 @@ class TimelineTranslations {
|
||||||
String? allowCommentsDescription,
|
String? allowCommentsDescription,
|
||||||
String? commentsTitleOnPost,
|
String? commentsTitleOnPost,
|
||||||
String? checkPost,
|
String? checkPost,
|
||||||
|
String? postAt,
|
||||||
String? deletePost,
|
String? deletePost,
|
||||||
String? deleteConfirmationTitle,
|
|
||||||
String? deleteConfirmationMessage,
|
|
||||||
String? deleteButton,
|
|
||||||
String? deleteCancelButton,
|
|
||||||
String? deleteReaction,
|
String? deleteReaction,
|
||||||
String? viewPost,
|
String? viewPost,
|
||||||
String? oneLikeTitle,
|
String? likesTitle,
|
||||||
String? multipleLikesTitle,
|
|
||||||
String? commentsTitle,
|
String? commentsTitle,
|
||||||
String? writeComment,
|
String? writeComment,
|
||||||
String? firstComment,
|
String? firstComment,
|
||||||
|
@ -197,15 +141,6 @@ class TimelineTranslations {
|
||||||
String? contentHintText,
|
String? contentHintText,
|
||||||
String? yes,
|
String? yes,
|
||||||
String? no,
|
String? no,
|
||||||
String? timeLineScreenTitle,
|
|
||||||
String? createCategoryPopuptitle,
|
|
||||||
String? addCategoryTitle,
|
|
||||||
String? addCategorySubmitButton,
|
|
||||||
String? addCategoryCancelButtton,
|
|
||||||
String? addCategoryHintText,
|
|
||||||
String? addCategoryErrorText,
|
|
||||||
String? titleErrorText,
|
|
||||||
String? contentErrorText,
|
|
||||||
}) =>
|
}) =>
|
||||||
TimelineTranslations(
|
TimelineTranslations(
|
||||||
noPosts: noPosts ?? this.noPosts,
|
noPosts: noPosts ?? this.noPosts,
|
||||||
|
@ -222,17 +157,11 @@ class TimelineTranslations {
|
||||||
allowCommentsDescription ?? this.allowCommentsDescription,
|
allowCommentsDescription ?? this.allowCommentsDescription,
|
||||||
commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost,
|
commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost,
|
||||||
checkPost: checkPost ?? this.checkPost,
|
checkPost: checkPost ?? this.checkPost,
|
||||||
|
postAt: postAt ?? this.postAt,
|
||||||
deletePost: deletePost ?? this.deletePost,
|
deletePost: deletePost ?? this.deletePost,
|
||||||
deleteConfirmationTitle:
|
|
||||||
deleteConfirmationTitle ?? this.deleteConfirmationTitle,
|
|
||||||
deleteConfirmationMessage:
|
|
||||||
deleteConfirmationMessage ?? this.deleteConfirmationMessage,
|
|
||||||
deleteButton: deleteButton ?? this.deleteButton,
|
|
||||||
deleteCancelButton: deleteCancelButton ?? this.deleteCancelButton,
|
|
||||||
deleteReaction: deleteReaction ?? this.deleteReaction,
|
deleteReaction: deleteReaction ?? this.deleteReaction,
|
||||||
viewPost: viewPost ?? this.viewPost,
|
viewPost: viewPost ?? this.viewPost,
|
||||||
oneLikeTitle: oneLikeTitle ?? this.oneLikeTitle,
|
likesTitle: likesTitle ?? this.likesTitle,
|
||||||
multipleLikesTitle: multipleLikesTitle ?? this.multipleLikesTitle,
|
|
||||||
commentsTitle: commentsTitle ?? this.commentsTitle,
|
commentsTitle: commentsTitle ?? this.commentsTitle,
|
||||||
writeComment: writeComment ?? this.writeComment,
|
writeComment: writeComment ?? this.writeComment,
|
||||||
firstComment: firstComment ?? this.firstComment,
|
firstComment: firstComment ?? this.firstComment,
|
||||||
|
@ -247,17 +176,5 @@ class TimelineTranslations {
|
||||||
contentHintText: contentHintText ?? this.contentHintText,
|
contentHintText: contentHintText ?? this.contentHintText,
|
||||||
yes: yes ?? this.yes,
|
yes: yes ?? this.yes,
|
||||||
no: no ?? this.no,
|
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,8 +11,6 @@ import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.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/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 {
|
class TimelinePostCreationScreen extends StatefulWidget {
|
||||||
const TimelinePostCreationScreen({
|
const TimelinePostCreationScreen({
|
||||||
|
@ -53,32 +51,52 @@ class _TimelinePostCreationScreenState
|
||||||
TextEditingController titleController = TextEditingController();
|
TextEditingController titleController = TextEditingController();
|
||||||
TextEditingController contentController = TextEditingController();
|
TextEditingController contentController = TextEditingController();
|
||||||
Uint8List? image;
|
Uint8List? image;
|
||||||
|
bool editingDone = false;
|
||||||
bool allowComments = false;
|
bool allowComments = false;
|
||||||
bool titleIsValid = false;
|
|
||||||
bool contentIsValid = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
titleController.addListener(_listenForInputs);
|
|
||||||
contentController.addListener(_listenForInputs);
|
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
titleController.addListener(checkIfEditingDone);
|
||||||
|
contentController.addListener(checkIfEditingDone);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenForInputs() {
|
@override
|
||||||
titleIsValid = titleController.text.isNotEmpty;
|
void dispose() {
|
||||||
contentIsValid = contentController.text.isNotEmpty;
|
titleController.dispose();
|
||||||
setState(() {});
|
contentController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
var formkey = GlobalKey<FormState>();
|
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!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var imageRequired = widget.options.requireImageForPost;
|
|
||||||
|
|
||||||
Future<void> onPostCreated() async {
|
Future<void> onPostCreated() async {
|
||||||
var user = await widget.service.userService?.getUser(widget.userId);
|
|
||||||
var post = TimelinePost(
|
var post = TimelinePost(
|
||||||
id: 'Post${Random().nextInt(1000)}',
|
id: 'Post${Random().nextInt(1000)}',
|
||||||
creatorId: widget.userId,
|
creatorId: widget.userId,
|
||||||
|
@ -86,12 +104,10 @@ class _TimelinePostCreationScreenState
|
||||||
category: widget.postCategory,
|
category: widget.postCategory,
|
||||||
content: contentController.text,
|
content: contentController.text,
|
||||||
likes: 0,
|
likes: 0,
|
||||||
likedBy: const [],
|
|
||||||
reaction: 0,
|
reaction: 0,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
reactionEnabled: allowComments,
|
reactionEnabled: allowComments,
|
||||||
image: image,
|
image: image,
|
||||||
creator: user,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.enablePostOverviewScreen) {
|
if (widget.enablePostOverviewScreen) {
|
||||||
|
@ -102,291 +118,207 @@ class _TimelinePostCreationScreenState
|
||||||
}
|
}
|
||||||
|
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => FocusScope.of(context).unfocus(),
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
child: SingleChildScrollView(
|
child: Padding(
|
||||||
child: Padding(
|
padding: widget.options.padding,
|
||||||
padding: widget.options.paddings.mainPadding,
|
child: SingleChildScrollView(
|
||||||
child: Form(
|
child: Column(
|
||||||
key: formkey,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.max,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
widget.options.translations.title,
|
||||||
Text(
|
style: theme.textTheme.titleMedium,
|
||||||
widget.options.translations.title,
|
),
|
||||||
style: theme.textTheme.titleMedium,
|
widget.options.textInputBuilder?.call(
|
||||||
),
|
titleController,
|
||||||
const SizedBox(
|
null,
|
||||||
height: 4,
|
'',
|
||||||
),
|
) ??
|
||||||
widget.options.textInputBuilder?.call(
|
TextField(
|
||||||
titleController,
|
controller: titleController,
|
||||||
null,
|
decoration: InputDecoration(
|
||||||
'',
|
|
||||||
) ??
|
|
||||||
PostCreationTextfield(
|
|
||||||
fieldKey: const ValueKey('title'),
|
|
||||||
controller: titleController,
|
|
||||||
hintText: widget.options.translations.titleHintText,
|
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: 24),
|
),
|
||||||
Text(
|
const SizedBox(height: 16),
|
||||||
widget.options.translations.content,
|
Text(
|
||||||
style: theme.textTheme.titleMedium,
|
widget.options.translations.content,
|
||||||
),
|
style: theme.textTheme.titleMedium,
|
||||||
Text(
|
),
|
||||||
widget.options.translations.contentDescription,
|
const SizedBox(height: 4),
|
||||||
style: theme.textTheme.bodySmall,
|
Text(
|
||||||
),
|
widget.options.translations.contentDescription,
|
||||||
const SizedBox(
|
style: theme.textTheme.bodyMedium,
|
||||||
height: 4,
|
),
|
||||||
),
|
// input field for the content
|
||||||
PostCreationTextfield(
|
SizedBox(
|
||||||
fieldKey: const ValueKey('content'),
|
height: 100,
|
||||||
|
child: TextField(
|
||||||
controller: contentController,
|
controller: contentController,
|
||||||
hintText: widget.options.translations.contentHintText,
|
|
||||||
textMaxLength: null,
|
|
||||||
decoration: widget.options.contentInputDecoration,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
expands: false,
|
expands: true,
|
||||||
minLines: null,
|
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
validator: (value) {
|
minLines: null,
|
||||||
if (value == null || value.isEmpty) {
|
decoration: InputDecoration(
|
||||||
return widget.options.translations.contentErrorText;
|
hintText: widget.options.translations.contentHintText,
|
||||||
}
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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(
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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: 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
// input field for the content
|
||||||
|
Text(
|
||||||
|
widget.options.translations.uploadImage,
|
||||||
|
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.background,
|
||||||
|
child: ImagePicker(
|
||||||
|
imagePickerConfig: widget.options.imagePickerConfig,
|
||||||
|
imagePickerTheme: widget.options.imagePickerTheme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
widget.options.translations.commentsTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.allowCommentsDescription,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Checkbox(
|
||||||
|
value: allowComments,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
allowComments = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(widget.options.translations.yes),
|
||||||
|
Checkbox(
|
||||||
|
value: !allowComments,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
allowComments = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(widget.options.translations.no),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: (widget.options.buttonBuilder != null)
|
||||||
|
? widget.options.buttonBuilder!(
|
||||||
|
context,
|
||||||
|
onPostCreated,
|
||||||
|
widget.options.translations.checkPost,
|
||||||
|
enabled: editingDone,
|
||||||
|
)
|
||||||
|
: ElevatedButton(
|
||||||
|
onPressed: editingDone
|
||||||
|
? () async {
|
||||||
|
await onPostCreated();
|
||||||
|
await widget.service.postService
|
||||||
|
.fetchPosts(null);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(
|
||||||
|
widget.enablePostOverviewScreen
|
||||||
|
? widget.options.translations.checkPost
|
||||||
|
: widget.options.translations.postCreation,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
// ignore_for_file: prefer_expression_function_bodies
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
|
|
||||||
class TimelinePostOverviewScreen extends StatelessWidget {
|
class TimelinePostOverviewScreen extends StatelessWidget {
|
||||||
const TimelinePostOverviewScreen({
|
const TimelinePostOverviewScreen({
|
||||||
|
@ -18,61 +19,38 @@ class TimelinePostOverviewScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var isSubmitted = false;
|
return Scaffold(
|
||||||
return Column(
|
appBar: AppBar(
|
||||||
mainAxisSize: MainAxisSize.max,
|
backgroundColor: Colors.black,
|
||||||
children: [
|
title: Text(
|
||||||
Expanded(
|
options.translations.postOverview,
|
||||||
child: TimelinePostScreen(
|
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||||
userId: timelinePost.creatorId,
|
|
||||||
options: options,
|
|
||||||
post: timelinePost,
|
|
||||||
onPostDelete: () async {},
|
|
||||||
service: service,
|
|
||||||
isOverviewScreen: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
options.postOverviewButtonBuilder?.call(
|
),
|
||||||
context,
|
body: Column(
|
||||||
() {
|
children: [
|
||||||
if (isSubmitted) return;
|
Flexible(
|
||||||
isSubmitted = true;
|
child: TimelinePostScreen(
|
||||||
|
userId: timelinePost.creatorId,
|
||||||
|
options: options,
|
||||||
|
post: timelinePost,
|
||||||
|
onPostDelete: () async {},
|
||||||
|
service: service,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 30.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
onPostSubmit(timelinePost);
|
onPostSubmit(timelinePost);
|
||||||
},
|
},
|
||||||
options.translations.postIn,
|
child: Text(
|
||||||
timelinePost,
|
'${options.translations.postIn} ${timelinePost.category}',
|
||||||
) ??
|
|
||||||
options.buttonBuilder?.call(
|
|
||||||
context,
|
|
||||||
() {
|
|
||||||
if (isSubmitted) return;
|
|
||||||
isSubmitted = true;
|
|
||||||
onPostSubmit(timelinePost);
|
|
||||||
},
|
|
||||||
options.translations.postIn,
|
|
||||||
enabled: true,
|
|
||||||
) ??
|
|
||||||
SafeArea(
|
|
||||||
bottom: true,
|
|
||||||
child: Padding(
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,26 +3,25 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
|
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
|
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class TimelinePostScreen extends StatefulWidget {
|
class TimelinePostScreen extends StatelessWidget {
|
||||||
const TimelinePostScreen({
|
const TimelinePostScreen({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.post,
|
required this.post,
|
||||||
required this.onPostDelete,
|
required this.onPostDelete,
|
||||||
this.allowAllDeletion = false,
|
|
||||||
this.isOverviewScreen = false,
|
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -30,10 +29,6 @@ class TimelinePostScreen extends StatefulWidget {
|
||||||
/// The user id of the current user
|
/// The user id of the current user
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
|
||||||
/// only the posts of the current user
|
|
||||||
final bool allowAllDeletion;
|
|
||||||
|
|
||||||
/// The timeline service to fetch the post details
|
/// The timeline service to fetch the post details
|
||||||
final TimelineService service;
|
final TimelineService service;
|
||||||
|
|
||||||
|
@ -48,16 +43,63 @@ class TimelinePostScreen extends StatefulWidget {
|
||||||
|
|
||||||
final VoidCallback onPostDelete;
|
final VoidCallback onPostDelete;
|
||||||
|
|
||||||
final bool? isOverviewScreen;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: _TimelinePostScreen(
|
||||||
|
userId: userId,
|
||||||
|
service: service,
|
||||||
|
options: options,
|
||||||
|
post: post,
|
||||||
|
onPostDelete: onPostDelete,
|
||||||
|
onUserTap: onUserTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
class _TimelinePostScreen extends StatefulWidget {
|
||||||
|
const _TimelinePostScreen({
|
||||||
|
required this.userId,
|
||||||
|
required this.service,
|
||||||
|
required this.options,
|
||||||
|
required this.post,
|
||||||
|
required this.onPostDelete,
|
||||||
|
this.onUserTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
final TimelineService service;
|
||||||
|
|
||||||
|
final TimelineOptions options;
|
||||||
|
|
||||||
|
final TimelinePost post;
|
||||||
|
|
||||||
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
|
final VoidCallback onPostDelete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TimelinePostScreen> createState() => _TimelinePostScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
||||||
TimelinePost? post;
|
TimelinePost? post;
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
|
late var textInputBuilder = widget.options.textInputBuilder ??
|
||||||
|
(controller, suffixIcon, hintText) => TextField(
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
suffixIcon: suffixIcon,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(20.0), // Adjust the value as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -74,7 +116,8 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
post = loadedPost;
|
post = loadedPost;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
} on Exception catch (_) {
|
} on Exception catch (e) {
|
||||||
|
debugPrint('Error loading post: $e');
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
|
@ -91,13 +134,12 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
var dateFormat = widget.options.dateFormat ??
|
var dateFormat = widget.options.dateFormat ??
|
||||||
DateFormat(
|
DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode);
|
||||||
"dd/MM/yyyy 'at' HH:mm",
|
var timeFormat = widget.options.timeFormat ?? DateFormat('HH:mm');
|
||||||
Localizations.localeOf(context).languageCode,
|
|
||||||
);
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const Center(
|
const Center(
|
||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.post == null) {
|
if (this.post == null) {
|
||||||
|
@ -114,49 +156,10 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
? a.createdAt.compareTo(b.createdAt)
|
? a.createdAt.compareTo(b.createdAt)
|
||||||
: b.createdAt.compareTo(a.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(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
RefreshIndicator.adaptive(
|
RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
updatePost(
|
updatePost(
|
||||||
await widget.service.postService.fetchPostDetails(
|
await widget.service.postService.fetchPostDetails(
|
||||||
|
@ -168,7 +171,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.options.paddings.postPadding,
|
padding: widget.options.padding,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -188,7 +191,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
28,
|
28,
|
||||||
) ??
|
) ??
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 14,
|
radius: 20,
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
CachedNetworkImageProvider(
|
CachedNetworkImageProvider(
|
||||||
post.creator!.imageUrl!,
|
post.creator!.imageUrl!,
|
||||||
|
@ -200,7 +203,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
28,
|
28,
|
||||||
) ??
|
) ??
|
||||||
const CircleAvatar(
|
const CircleAvatar(
|
||||||
radius: 14,
|
radius: 20,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
),
|
),
|
||||||
|
@ -214,27 +217,16 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
widget.options.translations.anonymousUser,
|
widget.options.translations.anonymousUser,
|
||||||
style: widget.options.theme.textStyles
|
style: widget.options.theme.textStyles
|
||||||
.postCreatorTitleStyle ??
|
.postCreatorTitleStyle ??
|
||||||
theme.textTheme.titleSmall!.copyWith(
|
theme.textTheme.titleMedium,
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (!(widget.isOverviewScreen ?? false) &&
|
if (widget.options.allowAllDeletion ||
|
||||||
(widget.allowAllDeletion ||
|
post.creator?.userId == widget.userId)
|
||||||
post.creator?.userId == widget.userId)) ...[
|
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
onSelected: (value) async {
|
onSelected: (value) => widget.onPostDelete(),
|
||||||
if (value == 'delete') {
|
|
||||||
await showPostDeletionConfirmationDialog(
|
|
||||||
widget.options,
|
|
||||||
context,
|
|
||||||
widget.onPostDelete,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (BuildContext context) =>
|
itemBuilder: (BuildContext context) =>
|
||||||
<PopupMenuEntry<String>>[
|
<PopupMenuEntry<String>>[
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
|
@ -263,11 +255,10 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
color: widget.options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// image of the posts
|
// image of the post
|
||||||
if (post.imageUrl != null || post.image != null) ...[
|
if (post.imageUrl != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
@ -302,17 +293,11 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
false;
|
false;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: post.image != null
|
: CachedNetworkImage(
|
||||||
? Image.memory(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
imageUrl: post.imageUrl!,
|
||||||
post.image!,
|
fit: BoxFit.fitHeight,
|
||||||
fit: BoxFit.fitHeight,
|
),
|
||||||
)
|
|
||||||
: CachedNetworkImage(
|
|
||||||
width: double.infinity,
|
|
||||||
imageUrl: post.imageUrl!,
|
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -321,70 +306,66 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
// post information
|
// post information
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
if (post.likedBy?.contains(widget.userId) ?? false) ...[
|
||||||
padding: EdgeInsets.zero,
|
InkWell(
|
||||||
constraints: const BoxConstraints(),
|
onTap: () async {
|
||||||
onPressed: () async {
|
|
||||||
if (widget.isOverviewScreen ?? false) return;
|
|
||||||
if (isLikedByUser) {
|
|
||||||
updatePost(
|
updatePost(
|
||||||
await widget.service.postService.unlikePost(
|
await widget.service.postService.unlikePost(
|
||||||
widget.userId,
|
widget.userId,
|
||||||
post,
|
post,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {});
|
},
|
||||||
} else {
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: widget.options.theme.likedIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.thumb_up_rounded,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
updatePost(
|
updatePost(
|
||||||
await widget.service.postService.likePost(
|
await widget.service.postService.likePost(
|
||||||
widget.userId,
|
widget.userId,
|
||||||
post,
|
post,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {});
|
},
|
||||||
}
|
child: Container(
|
||||||
},
|
color: Colors.transparent,
|
||||||
icon: isLikedByUser
|
child: widget.options.theme.likeIcon ??
|
||||||
? widget.options.theme.likedIcon ??
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.favorite_rounded,
|
Icons.thumb_up_alt_outlined,
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
size: widget.options.iconSize,
|
|
||||||
)
|
|
||||||
: widget.options.theme.likeIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_outline_outlined,
|
|
||||||
color: widget.options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
size: widget.options.iconSize,
|
size: widget.options.iconSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (post.reactionEnabled)
|
if (post.reactionEnabled)
|
||||||
widget.options.theme.commentIcon ??
|
widget.options.theme.commentIcon ??
|
||||||
SvgPicture.asset(
|
Icon(
|
||||||
'assets/Comment.svg',
|
Icons.chat_bubble_outline_rounded,
|
||||||
package: 'flutter_timeline_view',
|
|
||||||
// ignore: deprecated_member_use
|
|
||||||
color: widget.options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
width: widget.options.iconSize,
|
size: widget.options.iconSize,
|
||||||
height: widget.options.iconSize,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
Text(
|
||||||
if (widget.isOverviewScreen != null
|
'${post.likes} ${widget.options.translations.likesTitle}',
|
||||||
? !widget.isOverviewScreen!
|
style: widget
|
||||||
: false) ...[
|
.options.theme.textStyles.postLikeTitleAndAmount ??
|
||||||
Text(
|
theme.textTheme.titleSmall
|
||||||
// ignore: lines_longer_than_80_chars
|
?.copyWith(color: Colors.black),
|
||||||
'${post.likes} ${post.likes > 1 ? widget.options.translations.multipleLikesTitle : widget.options.translations.oneLikeTitle}',
|
),
|
||||||
style: widget.options.theme.textStyles
|
const SizedBox(height: 4),
|
||||||
.postLikeTitleAndAmount ??
|
|
||||||
theme.textTheme.titleSmall
|
|
||||||
?.copyWith(color: Colors.black),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Text.rich(
|
Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: widget.options.nameBuilder?.call(post.creator) ??
|
text: widget.options.nameBuilder?.call(post.creator) ??
|
||||||
|
@ -392,46 +373,66 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
widget.options.translations.anonymousUser,
|
widget.options.translations.anonymousUser,
|
||||||
style: widget
|
style: widget
|
||||||
.options.theme.textStyles.postCreatorNameStyle ??
|
.options.theme.textStyles.postCreatorNameStyle ??
|
||||||
theme.textTheme.titleSmall!
|
theme.textTheme.titleSmall,
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
children: [
|
children: [
|
||||||
|
const TextSpan(text: ' '),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: post.title,
|
text: post.title,
|
||||||
style:
|
style:
|
||||||
widget.options.theme.textStyles.postTitleStyle ??
|
widget.options.theme.textStyles.postTitleStyle ??
|
||||||
theme.textTheme.bodySmall,
|
theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
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(
|
Text(
|
||||||
post.content,
|
'${dateFormat.format(post.createdAt)} '
|
||||||
|
'${widget.options.translations.postAt} '
|
||||||
|
'${timeFormat.format(post.createdAt)}',
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(height: 20),
|
||||||
'${dateFormat.format(post.createdAt)} ',
|
if (post.reactionEnabled) ...[
|
||||||
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(
|
Text(
|
||||||
widget.options.translations.commentsTitleOnPost,
|
widget.options.translations.commentsTitleOnPost,
|
||||||
style: theme.textTheme.titleSmall!
|
style: theme.textTheme.titleMedium,
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
),
|
),
|
||||||
for (var reaction
|
for (var reaction
|
||||||
in post.reactions ?? <TimelinePostReaction>[]) ...[
|
in post.reactions ?? <TimelinePostReaction>[]) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 16),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPressStart: (details) async {
|
onLongPressStart: (details) async {
|
||||||
if (reaction.creatorId == widget.userId ||
|
if (reaction.creatorId == widget.userId ||
|
||||||
widget.allowAllDeletion) {
|
widget.options.allowAllDeletion) {
|
||||||
var overlay = Overlay.of(context)
|
var overlay = Overlay.of(context)
|
||||||
.context
|
.context
|
||||||
.findRenderObject()! as RenderBox;
|
.findRenderObject()! as RenderBox;
|
||||||
|
@ -465,16 +466,18 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: reaction.imageUrl != null
|
||||||
|
? CrossAxisAlignment.start
|
||||||
|
: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (reaction.creator?.imageUrl != null &&
|
if (reaction.creator?.imageUrl != null &&
|
||||||
reaction.creator!.imageUrl!.isNotEmpty) ...[
|
reaction.creator!.imageUrl!.isNotEmpty) ...[
|
||||||
widget.options.userAvatarBuilder?.call(
|
widget.options.userAvatarBuilder?.call(
|
||||||
reaction.creator!,
|
reaction.creator!,
|
||||||
14,
|
28,
|
||||||
) ??
|
) ??
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 14,
|
radius: 20,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
reaction.creator!.imageUrl!,
|
reaction.creator!.imageUrl!,
|
||||||
),
|
),
|
||||||
|
@ -482,10 +485,10 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
] else ...[
|
] else ...[
|
||||||
widget.options.anonymousAvatarBuilder?.call(
|
widget.options.anonymousAvatarBuilder?.call(
|
||||||
reaction.creator!,
|
reaction.creator!,
|
||||||
14,
|
28,
|
||||||
) ??
|
) ??
|
||||||
const CircleAvatar(
|
const CircleAvatar(
|
||||||
radius: 14,
|
radius: 20,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
),
|
),
|
||||||
|
@ -499,12 +502,11 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.options.nameBuilder
|
widget.options.nameBuilder
|
||||||
?.call(reaction.creator) ??
|
?.call(post.creator) ??
|
||||||
reaction.creator?.fullName ??
|
reaction.creator?.fullName ??
|
||||||
widget.options.translations
|
widget.options.translations
|
||||||
.anonymousUser,
|
.anonymousUser,
|
||||||
style: theme.textTheme.titleSmall!
|
style: theme.textTheme.titleSmall,
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
@ -521,95 +523,31 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: widget.options.nameBuilder
|
text: widget.options.nameBuilder
|
||||||
?.call(reaction.creator) ??
|
?.call(post.creator) ??
|
||||||
reaction.creator?.fullName ??
|
reaction.creator?.fullName ??
|
||||||
widget
|
widget
|
||||||
.options.translations.anonymousUser,
|
.options.translations.anonymousUser,
|
||||||
style: theme.textTheme.titleSmall!
|
style: theme.textTheme.titleSmall,
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(text: ' '),
|
const TextSpan(text: ' '),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: reaction.reaction ?? '',
|
text: reaction.reaction ?? '',
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
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
|
// 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) ...[
|
if (post.reactions?.isEmpty ?? true) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
widget.options.translations.firstComment,
|
widget.options.translations.firstComment,
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 120),
|
const SizedBox(height: 120),
|
||||||
|
@ -619,74 +557,53 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (post.reactionEnabled && !(widget.isOverviewScreen ?? false))
|
if (post.reactionEnabled)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Container(
|
child: ReactionBottom(
|
||||||
color: theme.scaffoldBackgroundColor,
|
messageInputBuilder: textInputBuilder,
|
||||||
constraints: BoxConstraints(
|
onPressSelectImage: () async {
|
||||||
maxWidth: MediaQuery.of(context).size.width,
|
// open the image picker
|
||||||
),
|
var result = await showModalBottomSheet<Uint8List?>(
|
||||||
child: SafeArea(
|
context: context,
|
||||||
bottom: true,
|
builder: (context) => Container(
|
||||||
child: Row(
|
padding: const EdgeInsets.all(8.0),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
color: theme.colorScheme.background,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: ImagePicker(
|
||||||
children: [
|
imagePickerConfig: widget.options.imagePickerConfig,
|
||||||
Padding(
|
imagePickerTheme: widget.options.imagePickerTheme,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Flexible(
|
),
|
||||||
child: Padding(
|
);
|
||||||
padding: const EdgeInsets.only(
|
if (result != null) {
|
||||||
left: 8,
|
updatePost(
|
||||||
right: 16,
|
await widget.service.postService.reactToPost(
|
||||||
top: 8,
|
post,
|
||||||
bottom: 8,
|
TimelinePostReaction(
|
||||||
),
|
id: '',
|
||||||
child: ReactionBottom(
|
postId: post.id,
|
||||||
messageInputBuilder: textInputBuilder,
|
creatorId: widget.userId,
|
||||||
onReactionSubmit: (reaction) async => updatePost(
|
createdAt: DateTime.now(),
|
||||||
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -9,34 +9,25 @@ import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||||
|
|
||||||
class TimelineScreen extends StatefulWidget {
|
class TimelineScreen extends StatefulWidget {
|
||||||
TimelineScreen({
|
const TimelineScreen({
|
||||||
this.userId = 'test_user',
|
this.userId = 'test_user',
|
||||||
TimelineService? service,
|
this.service,
|
||||||
this.options = const TimelineOptions(),
|
this.options = const TimelineOptions(),
|
||||||
this.onPostTap,
|
this.onPostTap,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
this.onRefresh,
|
|
||||||
this.posts,
|
this.posts,
|
||||||
this.timelineCategory,
|
this.timelineCategory,
|
||||||
this.postWidgetBuilder,
|
this.postWidgetBuilder,
|
||||||
this.filterEnabled = false,
|
this.filterEnabled = false,
|
||||||
this.allowAllDeletion = false,
|
|
||||||
super.key,
|
super.key,
|
||||||
}) : service = service ??
|
});
|
||||||
TimelineService(
|
|
||||||
postService: LocalTimelinePostService(),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The user id of the current user
|
/// The user id of the current user
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
|
||||||
/// only the posts of the current user
|
|
||||||
final bool allowAllDeletion;
|
|
||||||
|
|
||||||
/// The service to use for fetching and manipulating posts
|
/// The service to use for fetching and manipulating posts
|
||||||
final TimelineService service;
|
final TimelineService? service;
|
||||||
|
|
||||||
/// All the configuration options for the timelinescreens and widgets
|
/// All the configuration options for the timelinescreens and widgets
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
|
@ -54,9 +45,6 @@ class TimelineScreen extends StatefulWidget {
|
||||||
/// Called when a post is tapped
|
/// Called when a post is tapped
|
||||||
final Function(TimelinePost)? onPostTap;
|
final Function(TimelinePost)? onPostTap;
|
||||||
|
|
||||||
/// Called when the timeline is refreshed by pulling down
|
|
||||||
final Function(BuildContext context, String? category)? onRefresh;
|
|
||||||
|
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
/// If this is not null, the user can tap on the user avatar or name
|
||||||
final Function(String userId)? onUserTap;
|
final Function(String userId)? onUserTap;
|
||||||
|
|
||||||
|
@ -75,6 +63,10 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
late var textFieldController = TextEditingController(
|
late var textFieldController = TextEditingController(
|
||||||
text: widget.options.filterOptions.initialFilterWord,
|
text: widget.options.filterOptions.initialFilterWord,
|
||||||
);
|
);
|
||||||
|
late var service = widget.service ??
|
||||||
|
TimelineService(
|
||||||
|
postService: LocalTimelinePostService(),
|
||||||
|
);
|
||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
|
@ -93,7 +85,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
|
|
||||||
void _updateIsOnTop() {
|
void _updateIsOnTop() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isOnTop = controller.position.pixels < 0.1;
|
_isOnTop = controller.position.pixels < 40;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,32 +101,22 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant TimelineScreen oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
unawaited(loadPosts());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isLoading && widget.posts == null) {
|
if (isLoading && widget.posts == null) {
|
||||||
return const Center(child: CircularProgressIndicator.adaptive());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the list of posts
|
// Build the list of posts
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: widget.service.postService,
|
listenable: service.postService,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
if (!context.mounted) return const SizedBox();
|
if (!context.mounted) return const SizedBox();
|
||||||
var posts =
|
var posts = widget.posts ?? service.postService.getPosts(category);
|
||||||
widget.posts ?? widget.service.postService.getPosts(category);
|
|
||||||
|
|
||||||
if (widget.filterEnabled && filterWord != null) {
|
if (widget.filterEnabled && filterWord != null) {
|
||||||
if (widget.service.postService is TimelineFilterService) {
|
if (service.postService is TimelineFilterService) {
|
||||||
posts = (widget.service.postService as TimelineFilterService)
|
posts = (service.postService as TimelineFilterService)
|
||||||
.filterPosts(filterWord!, {});
|
.filterPosts(filterWord!, {});
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Timeline service needs to mixin'
|
debugPrint('Timeline service needs to mixin'
|
||||||
|
@ -157,19 +139,16 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var categories = widget.service.postService.categories;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: widget.options.paddings.mainPadding.top,
|
height: widget.options.padding.top,
|
||||||
),
|
),
|
||||||
if (widget.filterEnabled) ...[
|
if (widget.filterEnabled) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.symmetric(
|
||||||
left: widget.options.paddings.mainPadding.left,
|
horizontal: widget.options.padding.horizontal,
|
||||||
right: widget.options.paddings.mainPadding.right,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
@ -226,16 +205,11 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
CategorySelector(
|
CategorySelector(
|
||||||
categories: categories,
|
|
||||||
isOnTop: _isOnTop,
|
isOnTop: _isOnTop,
|
||||||
filter: category,
|
filter: category,
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
onTapCategory: (categoryKey) {
|
onTapCategory: (categoryKey) {
|
||||||
setState(() {
|
setState(() {
|
||||||
service.postService.selectedCategory =
|
|
||||||
categories.firstWhereOrNull(
|
|
||||||
(element) => element.key == categoryKey,
|
|
||||||
);
|
|
||||||
category = categoryKey;
|
category = categoryKey;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -244,90 +218,74 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator.adaptive(
|
child: SingleChildScrollView(
|
||||||
onRefresh: () async {
|
controller: controller,
|
||||||
await widget.onRefresh?.call(context, category);
|
child: Column(
|
||||||
await loadPosts();
|
mainAxisSize: MainAxisSize.min,
|
||||||
},
|
children: [
|
||||||
child: SingleChildScrollView(
|
...posts.map(
|
||||||
controller: controller,
|
(post) => Padding(
|
||||||
child: Column(
|
padding: widget.options.postPadding,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: widget.postWidgetBuilder?.call(post) ??
|
||||||
children: [
|
TimelinePostWidget(
|
||||||
/// Add a optional custom header to the list of posts
|
service: service,
|
||||||
widget.options.listHeaderBuilder
|
userId: widget.userId,
|
||||||
?.call(context, category) ??
|
options: widget.options,
|
||||||
const SizedBox.shrink(),
|
post: post,
|
||||||
...posts.map(
|
onTap: () async {
|
||||||
(post) => Padding(
|
if (widget.onPostTap != null) {
|
||||||
padding: widget.options.paddings.postPadding,
|
widget.onPostTap!.call(post);
|
||||||
child: widget.postWidgetBuilder?.call(post) ??
|
|
||||||
TimelinePostWidget(
|
|
||||||
service: widget.service,
|
|
||||||
userId: widget.userId,
|
|
||||||
options: widget.options,
|
|
||||||
allowAllDeletion: widget.allowAllDeletion,
|
|
||||||
post: post,
|
|
||||||
onTap: () async {
|
|
||||||
if (widget.onPostTap != null) {
|
|
||||||
widget.onPostTap!.call(post);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => Scaffold(
|
builder: (context) => Scaffold(
|
||||||
body: TimelinePostScreen(
|
body: TimelinePostScreen(
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
service: widget.service,
|
service: service,
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
post: post,
|
post: post,
|
||||||
onPostDelete: () {
|
onPostDelete: () {
|
||||||
widget.service.postService
|
service.postService.deletePost(post);
|
||||||
.deletePost(post);
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
onTapLike: () async => widget
|
},
|
||||||
.service.postService
|
onTapLike: () async => service.postService
|
||||||
.likePost(widget.userId, post),
|
.likePost(widget.userId, post),
|
||||||
onTapUnlike: () async => widget
|
onTapUnlike: () async => service.postService
|
||||||
.service.postService
|
.unlikePost(widget.userId, post),
|
||||||
.unlikePost(widget.userId, post),
|
onPostDelete: () async =>
|
||||||
onPostDelete: () async =>
|
service.postService.deletePost(post),
|
||||||
widget.service.postService.deletePost(post),
|
onUserTap: widget.onUserTap,
|
||||||
onUserTap: widget.onUserTap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (posts.isEmpty)
|
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
category == null
|
|
||||||
? widget.options.translations.noPosts
|
|
||||||
: widget
|
|
||||||
.options.translations.noPostsWithFilter,
|
|
||||||
style:
|
|
||||||
widget.options.theme.textStyles.noPostsStyle,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (posts.isEmpty)
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
category == null
|
||||||
|
? widget.options.translations.noPosts
|
||||||
|
: widget.options.translations.noPostsWithFilter,
|
||||||
|
style: widget.options.theme.textStyles.noPostsStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
|
||||||
height: widget.options.paddings.mainPadding.bottom,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: widget.options.padding.bottom,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -337,8 +295,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
Future<void> loadPosts() async {
|
Future<void> loadPosts() async {
|
||||||
if (widget.posts != null || !context.mounted) return;
|
if (widget.posts != null || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
await widget.service.postService.fetchCategories();
|
await service.postService.fetchPosts(category);
|
||||||
await widget.service.postService.fetchPosts(category);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/post_creation_textfield.dart';
|
|
||||||
|
|
||||||
class TimelineSelectionScreen extends StatefulWidget {
|
class TimelineSelectionScreen extends StatelessWidget {
|
||||||
const TimelineSelectionScreen({
|
const TimelineSelectionScreen({
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.categories,
|
required this.categories,
|
||||||
required this.onCategorySelected,
|
required this.onCategorySelected,
|
||||||
required this.postService,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,196 +16,52 @@ class TimelineSelectionScreen extends StatefulWidget {
|
||||||
|
|
||||||
final Function(TimelineCategory) onCategorySelected;
|
final Function(TimelineCategory) onCategorySelected;
|
||||||
|
|
||||||
final TimelinePostService postService;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TimelineSelectionScreen> createState() =>
|
|
||||||
_TimelineSelectionScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var size = MediaQuery.of(context).size;
|
var size = MediaQuery.of(context).size;
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: size.width * 0.05,
|
horizontal: size.width * 0.05,
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
Padding(
|
padding: EdgeInsets.only(top: size.height * 0.05, bottom: 8),
|
||||||
padding: const EdgeInsets.only(top: 20, bottom: 12),
|
child: Text(
|
||||||
child: Text(
|
options.translations.timelineSelectionDescription,
|
||||||
widget.options.translations.timelineSelectionDescription,
|
style:
|
||||||
style: theme.textTheme.titleLarge,
|
options.theme.textStyles.categorySelectionDescriptionStyle ??
|
||||||
),
|
theme.textTheme.displayMedium,
|
||||||
),
|
),
|
||||||
for (var category in widget.categories.where(
|
),
|
||||||
(element) => element.canCreate && element.key != null,
|
const SizedBox(height: 4),
|
||||||
)) ...[
|
for (var category in categories.where(
|
||||||
widget.options.categorySelectorButtonBuilder?.call(
|
(element) => element.canCreate,
|
||||||
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,
|
|
||||||
),
|
|
||||||
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(
|
InkWell(
|
||||||
onTap: showCategoryPopup,
|
onTap: () => onCategorySelected.call(category),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 60,
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
borderRadius: BorderRadius.circular(10),
|
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),
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Column(
|
vertical: 26,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
horizontal: 16,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
children: [
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
Padding(
|
child: Text(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
category.title,
|
||||||
child: Row(
|
style: options.theme.textStyles.categorySelectionTitleStyle ??
|
||||||
children: [
|
theme.textTheme.displaySmall,
|
||||||
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,12 +13,6 @@ class LocalTimelinePostService
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> posts = [];
|
List<TimelinePost> posts = [];
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelineCategory> categories = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimelineCategory? selectedCategory;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||||
posts.add(
|
posts.add(
|
||||||
|
@ -27,8 +21,8 @@ class LocalTimelinePostService
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
||||||
firstName: 'Ico',
|
firstName: 'Dirk',
|
||||||
lastName: 'Nica',
|
lastName: 'lukassen',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -124,11 +118,10 @@ class LocalTimelinePostService
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost?> getPost(String postId) => Future.value(
|
TimelinePost? getPost(String postId) =>
|
||||||
(posts.any((element) => element.id == postId))
|
(posts.any((element) => element.id == postId))
|
||||||
? posts.firstWhere((element) => element.id == postId)
|
? posts.firstWhere((element) => element.id == postId)
|
||||||
: null,
|
: null;
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> getPosts(String? category) => posts
|
List<TimelinePost> getPosts(String? category) => posts
|
||||||
|
@ -181,8 +174,8 @@ class LocalTimelinePostService
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
||||||
firstName: 'Ico',
|
firstName: 'Dirk',
|
||||||
lastName: 'Nica',
|
lastName: 'lukassen',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -204,129 +197,64 @@ class LocalTimelinePostService
|
||||||
TimelinePost(
|
TimelinePost(
|
||||||
id: 'Post0',
|
id: 'Post0',
|
||||||
creatorId: 'test_user',
|
creatorId: 'test_user',
|
||||||
title: 'De topper van de maand september',
|
title: 'Post 0',
|
||||||
category: 'Category',
|
category: null,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://firebasestorage.googleapis.com/v0/b/appshell-demo.appspot.com/o/do_not_delete_1.png?alt=media&token=e4b2f9f3-c81f-4ac7-a938-e846691399f7',
|
'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg',
|
||||||
content: 'Dit is onze topper van de maand september! Gefeliciteerd!',
|
content: 'Standard post without image made by the current user',
|
||||||
likes: 72,
|
likes: 0,
|
||||||
reaction: 0,
|
reaction: 0,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
reactionEnabled: true,
|
reactionEnabled: false,
|
||||||
creator: const TimelinePosterUserModel(
|
creator: const TimelinePosterUserModel(
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://firebasestorage.googleapis.com/v0/b/appshell-demo.appspot.com/o/do_not_delete_3.png?alt=media&token=cd7c156d-0dda-43be-9199-f7d31c30132e',
|
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
||||||
firstName: 'Robin',
|
firstName: 'Dirk',
|
||||||
lastName: 'De Vries',
|
lastName: 'lukassen',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TimelinePost(
|
TimelinePost(
|
||||||
id: 'Post1',
|
id: 'Post1',
|
||||||
creatorId: 'test_user2',
|
creatorId: 'test_user2',
|
||||||
title: 'De soep van de week is: Aspergesoep',
|
title: 'Post 1',
|
||||||
category: 'Category with two lines',
|
category: null,
|
||||||
content:
|
content: 'Standard post with image made by a different user and '
|
||||||
'Aspergesoep is echt een heerlijke delicatesse. Deze soep wordt'
|
'reactions enabled',
|
||||||
' vaak gemaakt met verse asperges, bouillon en wat kruiden voor'
|
likes: 0,
|
||||||
' smaak. Het is een perfecte keuze voor een lichte en smaakvolle'
|
reaction: 0,
|
||||||
' maaltijd, vooral in het voorjaar wanneer asperges in seizoen'
|
createdAt: DateTime.now(),
|
||||||
' zijn. We serveren het met een vleugje room en wat knapperige'
|
reactionEnabled: false,
|
||||||
' croutons voor die extra touch.',
|
imageUrl:
|
||||||
likes: 72,
|
'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg',
|
||||||
|
creator: const TimelinePosterUserModel(
|
||||||
|
userId: 'test_user',
|
||||||
|
imageUrl:
|
||||||
|
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
||||||
|
firstName: 'Dirk',
|
||||||
|
lastName: 'lukassen',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TimelinePost(
|
||||||
|
id: 'Post2',
|
||||||
|
creatorId: 'test_user',
|
||||||
|
title: 'Post 2',
|
||||||
|
category: null,
|
||||||
|
content: 'Standard post with image made by the current user and'
|
||||||
|
' reactions enabled',
|
||||||
|
likes: 0,
|
||||||
reaction: 0,
|
reaction: 0,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
reactionEnabled: true,
|
reactionEnabled: true,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://firebasestorage.googleapis.com/v0/b/appshell-demo.appspot.com/o/do_not_delete_2.png?alt=media&token=ee4a8771-531f-4d1d-8613-a2366771e775',
|
'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg',
|
||||||
creator: const TimelinePosterUserModel(
|
creator: const TimelinePosterUserModel(
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://firebasestorage.googleapis.com/v0/b/appshell-demo.appspot.com/o/do_not_delete_4.png?alt=media&token=775d4d10-6d2b-4aef-a51b-ba746b7b137f',
|
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
||||||
firstName: 'Elise',
|
firstName: 'Dirk',
|
||||||
lastName: 'Welling',
|
lastName: 'lukassen',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@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,7 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.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/flutter_timeline_view.dart';
|
||||||
|
|
||||||
class CategorySelector extends StatefulWidget {
|
class CategorySelector extends StatefulWidget {
|
||||||
|
@ -10,7 +9,6 @@ class CategorySelector extends StatefulWidget {
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.onTapCategory,
|
required this.onTapCategory,
|
||||||
required this.isOnTop,
|
required this.isOnTop,
|
||||||
required this.categories,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,7 +16,6 @@ class CategorySelector extends StatefulWidget {
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
final void Function(String? categoryKey) onTapCategory;
|
final void Function(String? categoryKey) onTapCategory;
|
||||||
final bool isOnTop;
|
final bool isOnTop;
|
||||||
final List<TimelineCategory> categories;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CategorySelector> createState() => _CategorySelectorState();
|
State<CategorySelector> createState() => _CategorySelectorState();
|
||||||
|
@ -26,42 +23,47 @@ class CategorySelector extends StatefulWidget {
|
||||||
|
|
||||||
class _CategorySelectorState extends State<CategorySelector> {
|
class _CategorySelectorState extends State<CategorySelector> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => SingleChildScrollView(
|
Widget build(BuildContext context) {
|
||||||
scrollDirection: Axis.horizontal,
|
if (widget.options.categoriesOptions.categoriesBuilder == null) {
|
||||||
child: Padding(
|
return const SizedBox.shrink();
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
}
|
||||||
child: Row(
|
|
||||||
children: [
|
var categories =
|
||||||
SizedBox(
|
widget.options.categoriesOptions.categoriesBuilder!(context);
|
||||||
width: widget.options.categoriesOptions
|
return SingleChildScrollView(
|
||||||
.categorySelectorHorizontalPadding ??
|
scrollDirection: Axis.horizontal,
|
||||||
max(widget.options.paddings.mainPadding.left - 20, 0),
|
child: Row(
|
||||||
),
|
children: [
|
||||||
for (var category in widget.categories) ...[
|
SizedBox(
|
||||||
widget.options.categoriesOptions.categoryButtonBuilder?.call(
|
width: widget.options.categoriesOptions
|
||||||
category,
|
.categorySelectorHorizontalPadding ??
|
||||||
() => widget.onTapCategory(category.key),
|
max(widget.options.padding.horizontal - 20, 0),
|
||||||
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(
|
||||||
|
categoryKey: category.key,
|
||||||
|
categoryName: category.title,
|
||||||
|
onTap: () => widget.onTapCategory(category.key),
|
||||||
|
selected: widget.filter == category.key,
|
||||||
|
) ??
|
||||||
|
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.padding.horizontal - 4, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,130 +14,65 @@ class CategorySelectorButton extends StatelessWidget {
|
||||||
|
|
||||||
final TimelineCategory category;
|
final TimelineCategory category;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final VoidCallback onTap;
|
final void Function() onTap;
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
final bool isOnTop;
|
final bool isOnTop;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
var size = MediaQuery.of(context).size;
|
|
||||||
|
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
height: isOnTop ? 140 : 40,
|
height: isOnTop ? 140 : 40,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
padding: const WidgetStatePropertyAll(
|
padding: const MaterialStatePropertyAll(
|
||||||
EdgeInsets.symmetric(
|
EdgeInsets.symmetric(
|
||||||
vertical: 5,
|
vertical: 5,
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
fixedSize: WidgetStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
|
fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
selected
|
selected ? theme.colorScheme.primary : Colors.transparent,
|
||||||
? theme.colorScheme.primary
|
|
||||||
: options.theme.categorySelectionButtonBackgroundColor ??
|
|
||||||
Colors.transparent,
|
|
||||||
),
|
),
|
||||||
shape: WidgetStatePropertyAll(
|
shape: MaterialStatePropertyAll(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: options.theme.categorySelectionButtonBorderColor ??
|
color: theme.colorScheme.primary,
|
||||||
theme.colorScheme.primary,
|
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: isOnTop
|
child: Row(
|
||||||
? SizedBox(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
width: size.width,
|
children: [
|
||||||
child: Stack(
|
Column(
|
||||||
children: [
|
mainAxisAlignment:
|
||||||
Column(
|
isOnTop ? MainAxisAlignment.end : MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
category.title,
|
||||||
_CategoryButtonText(
|
style: (options.theme.textStyles.categoryTitleStyle ??
|
||||||
category: category,
|
theme.textTheme.labelLarge)
|
||||||
options: options,
|
?.copyWith(
|
||||||
theme: theme,
|
color: selected
|
||||||
selected: selected,
|
? theme.colorScheme.onPrimary
|
||||||
),
|
: theme.colorScheme.onSurface,
|
||||||
],
|
|
||||||
),
|
|
||||||
Center(child: category.icon),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (category.icon != null) ...[
|
|
||||||
category.icon!,
|
|
||||||
SizedBox(
|
|
||||||
width:
|
|
||||||
options.paddings.categoryButtonTextPadding ?? 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Expanded(
|
|
||||||
child: _CategoryButtonText(
|
|
||||||
category: category,
|
|
||||||
options: options,
|
|
||||||
theme: theme,
|
|
||||||
selected: selected,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CategoryButtonText extends StatelessWidget {
|
|
||||||
const _CategoryButtonText({
|
|
||||||
required this.category,
|
|
||||||
required this.options,
|
|
||||||
required this.theme,
|
|
||||||
required this.selected,
|
|
||||||
this.overflow,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TimelineCategory category;
|
|
||||||
final TimelineOptions options;
|
|
||||||
final ThemeData theme;
|
|
||||||
final bool selected;
|
|
||||||
final TextOverflow? overflow;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Text(
|
|
||||||
category.title,
|
|
||||||
style: (options.theme.textStyles.categoryTitleStyle ??
|
|
||||||
(selected
|
|
||||||
? theme.textTheme.titleMedium
|
|
||||||
: theme.textTheme.bodyMedium))
|
|
||||||
?.copyWith(
|
|
||||||
color: selected
|
|
||||||
? options.theme.categorySelectionButtonSelectedTextColor ??
|
|
||||||
theme.colorScheme.onPrimary
|
|
||||||
: options.theme.categorySelectionButtonUnselectedTextColor ??
|
|
||||||
theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
overflow: overflow,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
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,7 +3,6 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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_options.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
||||||
|
|
||||||
|
@ -12,12 +11,14 @@ class ReactionBottom extends StatefulWidget {
|
||||||
required this.onReactionSubmit,
|
required this.onReactionSubmit,
|
||||||
required this.messageInputBuilder,
|
required this.messageInputBuilder,
|
||||||
required this.translations,
|
required this.translations,
|
||||||
|
this.onPressSelectImage,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Future<void> Function(String text) onReactionSubmit;
|
final Future<void> Function(String text) onReactionSubmit;
|
||||||
final TextInputBuilder messageInputBuilder;
|
final TextInputBuilder messageInputBuilder;
|
||||||
|
final VoidCallback? onPressSelectImage;
|
||||||
final TimelineTranslations translations;
|
final TimelineTranslations translations;
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
|
|
||||||
|
@ -29,30 +30,44 @@ class _ReactionBottomState extends State<ReactionBottom> {
|
||||||
final TextEditingController _textEditingController = TextEditingController();
|
final TextEditingController _textEditingController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Container(
|
Widget build(BuildContext context) => SafeArea(
|
||||||
child: widget.messageInputBuilder(
|
bottom: true,
|
||||||
_textEditingController,
|
child: Container(
|
||||||
Padding(
|
color: Theme.of(context).colorScheme.background,
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Container(
|
||||||
horizontal: 8,
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
height: 48,
|
||||||
onPressed: () async {
|
child: widget.messageInputBuilder(
|
||||||
var value = _textEditingController.text;
|
_textEditingController,
|
||||||
if (value.isNotEmpty) {
|
Padding(
|
||||||
await widget.onReactionSubmit(value);
|
padding: const EdgeInsets.symmetric(
|
||||||
_textEditingController.clear();
|
horizontal: 4,
|
||||||
}
|
),
|
||||||
},
|
child: Row(
|
||||||
icon: SvgPicture.asset(
|
mainAxisSize: MainAxisSize.min,
|
||||||
'assets/send.svg',
|
children: [
|
||||||
package: 'flutter_timeline_view',
|
IconButton(
|
||||||
// ignore: deprecated_member_use
|
onPressed: () async {
|
||||||
color: widget.iconColor,
|
var value = _textEditingController.text;
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
await widget.onReactionSubmit(value);
|
||||||
|
_textEditingController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.send,
|
||||||
|
color: widget.iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
widget.translations.writeComment,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
widget.translations.writeComment,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,17 +99,11 @@ class _TappableImageState extends State<TappableImage>
|
||||||
offset: Offset(0, animation.value * -32),
|
offset: Offset(0, animation.value * -32),
|
||||||
child: Transform.scale(
|
child: Transform.scale(
|
||||||
scale: 1 + animation.value * 0.1,
|
scale: 1 + animation.value * 0.1,
|
||||||
child: widget.post.imageUrl != null
|
child: CachedNetworkImage(
|
||||||
? CachedNetworkImage(
|
imageUrl: widget.post.imageUrl ?? '',
|
||||||
imageUrl: widget.post.imageUrl ?? '',
|
width: double.infinity,
|
||||||
width: double.infinity,
|
fit: BoxFit.fitHeight,
|
||||||
fit: BoxFit.fitHeight,
|
),
|
||||||
)
|
|
||||||
: Image.memory(
|
|
||||||
width: double.infinity,
|
|
||||||
widget.post.image!,
|
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -141,7 +135,6 @@ class _HeartAnimationState extends State<HeartAnimation> {
|
||||||
unawaited(
|
unawaited(
|
||||||
Future.delayed(const Duration(milliseconds: 100)).then((value) async {
|
Future.delayed(const Duration(milliseconds: 100)).then((value) async {
|
||||||
active = widget.liked;
|
active = widget.liked;
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
var navigator = Navigator.of(context);
|
var navigator = Navigator.of(context);
|
||||||
await Future.delayed(widget.duration);
|
await Future.delayed(widget.duration);
|
||||||
navigator.pop();
|
navigator.pop();
|
||||||
|
|
|
@ -4,10 +4,8 @@
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.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_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
||||||
|
|
||||||
class TimelinePostWidget extends StatefulWidget {
|
class TimelinePostWidget extends StatefulWidget {
|
||||||
|
@ -20,18 +18,12 @@ class TimelinePostWidget extends StatefulWidget {
|
||||||
required this.onTapUnlike,
|
required this.onTapUnlike,
|
||||||
required this.onPostDelete,
|
required this.onPostDelete,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.allowAllDeletion,
|
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The user id of the current user
|
/// The user id of the current user
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
|
||||||
/// only the posts of the current user
|
|
||||||
final bool allowAllDeletion;
|
|
||||||
|
|
||||||
final TimelineOptions options;
|
final TimelineOptions options;
|
||||||
|
|
||||||
final TimelinePost post;
|
final TimelinePost post;
|
||||||
|
@ -54,391 +46,282 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false;
|
return InkWell(
|
||||||
|
onTap: widget.onTap,
|
||||||
return SizedBox(
|
child: SizedBox(
|
||||||
height: widget.post.imageUrl != null || widget.post.image != null
|
height: widget.post.imageUrl != null
|
||||||
? widget.options.postWidgetHeight
|
? widget.options.postWidgetHeight
|
||||||
: null,
|
: null,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.post.creator != null) ...[
|
if (widget.post.creator != null)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: widget.onUserTap != null
|
onTap: widget.onUserTap != null
|
||||||
? () =>
|
? () =>
|
||||||
widget.onUserTap?.call(widget.post.creator!.userId)
|
widget.onUserTap?.call(widget.post.creator!.userId)
|
||||||
: null,
|
: null,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.post.creator!.imageUrl != null) ...[
|
if (widget.post.creator!.imageUrl != null) ...[
|
||||||
widget.options.userAvatarBuilder?.call(
|
widget.options.userAvatarBuilder?.call(
|
||||||
widget.post.creator!,
|
widget.post.creator!,
|
||||||
28,
|
28,
|
||||||
) ??
|
) ??
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 14,
|
radius: 20,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
widget.post.creator!.imageUrl!,
|
widget.post.creator!.imageUrl!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
] else ...[
|
||||||
] else ...[
|
widget.options.anonymousAvatarBuilder?.call(
|
||||||
widget.options.anonymousAvatarBuilder?.call(
|
widget.post.creator!,
|
||||||
widget.post.creator!,
|
40,
|
||||||
28,
|
) ??
|
||||||
) ??
|
const CircleAvatar(
|
||||||
const CircleAvatar(
|
radius: 20,
|
||||||
radius: 14,
|
child: Icon(
|
||||||
child: Icon(
|
Icons.person,
|
||||||
Icons.person,
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
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.options.allowAllDeletion ||
|
||||||
|
widget.post.creator?.userId == widget.userId)
|
||||||
|
PopupMenuButton(
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'delete') {
|
||||||
|
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) ...[
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: CachedNetworkImage(
|
||||||
|
width: double.infinity,
|
||||||
|
imageUrl: widget.post.imageUrl!,
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
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.likedIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.favorite_outline,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: widget.options.theme.moreIcon ??
|
const SizedBox(width: 8),
|
||||||
Icon(
|
if (widget.post.reactionEnabled) ...[
|
||||||
Icons.more_horiz_rounded,
|
Container(
|
||||||
color: widget.options.theme.iconColor,
|
color: Colors.transparent,
|
||||||
),
|
child: widget.options.theme.commentIcon ??
|
||||||
),
|
Icon(
|
||||||
],
|
Icons.chat_bubble_outline_rounded,
|
||||||
],
|
color: widget.options.theme.iconColor,
|
||||||
),
|
size: widget.options.iconSize,
|
||||||
// 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}'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
] 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(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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
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: 8,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
InkWell(
|
if (widget.options.itemInfoBuilder != null) ...[
|
||||||
onTap: widget.onTap,
|
widget.options.itemInfoBuilder!(
|
||||||
child: Text(
|
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: 4),
|
||||||
|
Text(
|
||||||
widget.options.translations.viewPost,
|
widget.options.translations.viewPost,
|
||||||
style: widget.options.theme.textStyles.viewPostStyle ??
|
style: widget.options.theme.textStyles.viewPostStyle ??
|
||||||
theme.textTheme.titleSmall!.copyWith(
|
theme.textTheme.bodySmall,
|
||||||
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(
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
onPostDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,35 +4,38 @@
|
||||||
|
|
||||||
name: flutter_timeline_view
|
name: flutter_timeline_view
|
||||||
description: Visual elements of the Flutter Timeline Component
|
description: Visual elements of the Flutter Timeline Component
|
||||||
version: 5.1.1
|
version: 2.3.1
|
||||||
homepage: https://github.com/Iconica-Development/flutter_timeline
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.4.3 <4.0.0"
|
sdk: '>=3.1.3 <4.0.0'
|
||||||
flutter: '>=3.22.2'
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
intl: ^0.19.0
|
intl: any
|
||||||
cached_network_image: ^3.2.2
|
cached_network_image: ^3.2.2
|
||||||
dotted_border: ^2.1.0
|
dotted_border: ^2.1.0
|
||||||
collection: ^1.18.0
|
flutter_html: ^3.0.0-beta.2
|
||||||
flutter_svg: ^2.0.10+1
|
|
||||||
flutter_timeline_interface:
|
flutter_timeline_interface:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
git:
|
||||||
version: ^5.1.1
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
|
path: packages/flutter_timeline_interface
|
||||||
|
ref: 2.3.1
|
||||||
flutter_image_picker:
|
flutter_image_picker:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
git:
|
||||||
version: ^4.0.0
|
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||||
|
ref: 1.0.5
|
||||||
|
collection: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
flutter_iconica_analysis:
|
flutter_iconica_analysis:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
ref: 6.0.0
|
ref: 6.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
|
||||||
- assets/
|
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
name: flutter_timeline_workspace
|
name: flutter_timeline_workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.4.3 <4.0.0'
|
sdk: '>=2.18.0 <3.0.0'
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
melos: ^3.0.1
|
melos: ^3.0.1
|
||||||
|
|
Loading…
Reference in a new issue