mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 02:23:46 +02:00
Merge f0c1c265b3
into a62935eb60
This commit is contained in:
commit
2e572cdb50
108 changed files with 3102 additions and 5520 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
## 6.0.0
|
||||||
|
* Refactor the timeline package to use the new structure
|
||||||
|
|
||||||
## 5.1.0
|
## 5.1.0
|
||||||
|
|
||||||
* Added `routeToPostDetail` to the `TimelineUserStory` to allow for navigation to the post detail screen.
|
* Added `routeToPostDetail` to the `TimelineUserStory` to allow for navigation to the post detail screen.
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
List of Features from this component:
|
|
|
@ -24,9 +24,6 @@ If you are going to use Firebase as the back-end of the Timeline, you should als
|
||||||
```
|
```
|
||||||
|
|
||||||
In firebase add firestore and storage to your project.
|
In firebase add firestore and storage to your project.
|
||||||
In firestore add a collection named `timeline` and a collection named `users`.
|
|
||||||
In the `timeline` collection all posts will be stored. In the `users` collection all users will be stored.
|
|
||||||
In the `users` collection you should add your users data.
|
|
||||||
|
|
||||||
Add the following code in your `main` function, before the runApp().
|
Add the following code in your `main` function, before the runApp().
|
||||||
And import this package: import 'package:intl/date_symbol_data_local.dart';
|
And import this package: import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
|
29
packages/firebase_timeline_repository/.gitignore
vendored
Normal file
29
packages/firebase_timeline_repository/.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
1
packages/firebase_timeline_repository/CONTRIBUTING.md
Symbolic link
1
packages/firebase_timeline_repository/CONTRIBUTING.md
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../CONTRIBUTING.md
|
|
@ -0,0 +1,7 @@
|
||||||
|
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
|
@ -0,0 +1,7 @@
|
||||||
|
/// firebase_timeline_repository is a library for Firebase repositories.
|
||||||
|
library firebase_timeline_repository;
|
||||||
|
|
||||||
|
/// Firebase repositories
|
||||||
|
export "src/firebase_category_repository.dart";
|
||||||
|
export "src/firebase_post_repository.dart";
|
||||||
|
export "src/firebase_user_repository.dart";
|
|
@ -0,0 +1,73 @@
|
||||||
|
import "package:cloud_firestore/cloud_firestore.dart";
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class FirebaseCategoryRepository implements CategoryRepositoryInterface {
|
||||||
|
final CollectionReference categoryCollection =
|
||||||
|
FirebaseFirestore.instance.collection("timeline_categories");
|
||||||
|
|
||||||
|
final List<TimelineCategory> _categories = [];
|
||||||
|
TimelineCategory? _selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createCategory(TimelineCategory category) async {
|
||||||
|
await categoryCollection.add(category.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<TimelineCategory>> getCategories() {
|
||||||
|
var currentlySelected = _selectedCategory;
|
||||||
|
|
||||||
|
return categoryCollection
|
||||||
|
.snapshots()
|
||||||
|
.map(
|
||||||
|
(snapshot) => snapshot.docs
|
||||||
|
.map(
|
||||||
|
(doc) => TimelineCategory.fromJson(
|
||||||
|
doc.data()! as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
.map((categories) {
|
||||||
|
// Ensure that selected category is preserved during re-fetching
|
||||||
|
|
||||||
|
// Modify _categories without resetting selected category
|
||||||
|
if (_categories.isEmpty) {
|
||||||
|
_categories.add(
|
||||||
|
const TimelineCategory(
|
||||||
|
key: null,
|
||||||
|
title: "All",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_categories.addAll(categories);
|
||||||
|
} else {
|
||||||
|
_categories
|
||||||
|
..clear()
|
||||||
|
..add(const TimelineCategory(key: null, title: "All"))
|
||||||
|
..addAll(categories);
|
||||||
|
_selectedCategory = _categories.firstWhereOrNull(
|
||||||
|
(category) => category.key == currentlySelected?.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _categories;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineCategory? getCategory(String? categoryId) =>
|
||||||
|
_categories.firstWhereOrNull(
|
||||||
|
(category) => category.key == categoryId,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineCategory? getSelectedCategory() => _selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineCategory? selectCategory(String? categoryId) {
|
||||||
|
_selectedCategory = _categories.firstWhereOrNull(
|
||||||
|
(category) => category.key == categoryId,
|
||||||
|
);
|
||||||
|
return _selectedCategory;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
import "dart:async";
|
||||||
|
import "dart:typed_data";
|
||||||
|
|
||||||
|
import "package:cloud_firestore/cloud_firestore.dart";
|
||||||
|
import "package:firebase_storage/firebase_storage.dart";
|
||||||
|
import "package:firebase_timeline_repository/firebase_timeline_repository.dart";
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class FirebasePostRepository implements PostRepositoryInterface {
|
||||||
|
FirebasePostRepository({
|
||||||
|
this.userService,
|
||||||
|
}) {
|
||||||
|
userService ??= FirebaseUserRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineUserRepositoryInterface? userService;
|
||||||
|
|
||||||
|
final CollectionReference postCollection =
|
||||||
|
FirebaseFirestore.instance.collection("timeline");
|
||||||
|
|
||||||
|
late TimelinePost? _currentPost;
|
||||||
|
|
||||||
|
final List<TimelinePost> _posts = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createPost(TimelinePost post) async {
|
||||||
|
var updatedPost = post;
|
||||||
|
if (post.image != null) {
|
||||||
|
// add image upload logic here
|
||||||
|
var imageRef = FirebaseStorage.instance.ref().child("timeline/$post.id");
|
||||||
|
var result = await imageRef.putData(post.image!);
|
||||||
|
var imageUrl = await result.ref.getDownloadURL();
|
||||||
|
updatedPost = post.copyWith(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_posts.add(updatedPost);
|
||||||
|
|
||||||
|
await postCollection.add(updatedPost.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction, {
|
||||||
|
Uint8List? image,
|
||||||
|
}) async {
|
||||||
|
var user = await userService!.getCurrentUser();
|
||||||
|
var currentReaction = reaction.copyWith(
|
||||||
|
creatorId: user.userId,
|
||||||
|
creator: user,
|
||||||
|
);
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reaction: post.reaction + 1,
|
||||||
|
reactions: post.reactions
|
||||||
|
?..add(
|
||||||
|
currentReaction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await postCollection.doc(post.id).update(updatedPost.toJson());
|
||||||
|
_posts[_posts.indexWhere((element) => element.id == post.id)] = updatedPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deletePost(String id) async {
|
||||||
|
await postCollection.doc(id).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelinePost getCurrentPost() => _currentPost!;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<TimelinePost>> getPosts(String? categoryId) => postCollection
|
||||||
|
.where("category", isEqualTo: categoryId)
|
||||||
|
.snapshots()
|
||||||
|
.asyncMap((snapshot) async {
|
||||||
|
// Fetch posts and their associated users
|
||||||
|
var posts = await Future.wait(
|
||||||
|
snapshot.docs.map((doc) async {
|
||||||
|
// Get user who created the post
|
||||||
|
var postData = doc.data()! as Map<String, dynamic>;
|
||||||
|
var user = await userService!.getUser(postData["creator_id"]);
|
||||||
|
|
||||||
|
// Create post from document data
|
||||||
|
var post = TimelinePost.fromJson(doc.id, postData);
|
||||||
|
|
||||||
|
// Update reactions with user details
|
||||||
|
if (post.reactions != null) {
|
||||||
|
post = post.copyWith(
|
||||||
|
reactions: await Future.wait(
|
||||||
|
post.reactions!.map((reaction) async {
|
||||||
|
var reactionUser =
|
||||||
|
await userService!.getUser(reaction.creatorId);
|
||||||
|
return reaction.copyWith(creator: reactionUser);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return post with creator information
|
||||||
|
return post.copyWith(creator: user);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update internal post list
|
||||||
|
_posts
|
||||||
|
..clear()
|
||||||
|
..addAll(posts);
|
||||||
|
|
||||||
|
return _posts;
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> likePost(String postId, String userId) async {
|
||||||
|
var post = await postCollection.doc(postId).get();
|
||||||
|
var updatedPost =
|
||||||
|
TimelinePost.fromJson(post.id, post.data()! as Map<String, dynamic>);
|
||||||
|
updatedPost = updatedPost.copyWith(
|
||||||
|
likes: updatedPost.likes + 1,
|
||||||
|
likedBy: updatedPost.likedBy?..add(userId),
|
||||||
|
);
|
||||||
|
await postCollection.doc(postId).update(updatedPost.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> likePostReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reaction: post.reaction + 1,
|
||||||
|
reactions: post.reactions
|
||||||
|
?..[post.reactions!
|
||||||
|
.indexWhere((element) => element.id == reaction.id)] =
|
||||||
|
reaction.copyWith(
|
||||||
|
likedBy: reaction.likedBy?..add(userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await postCollection.doc(post.id).update(updatedPost.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setCurrentPost(TimelinePost post) {
|
||||||
|
_currentPost = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> unlikePost(String postId, String userId) async {
|
||||||
|
var updatedPost = _posts.firstWhere((element) => element.id == postId);
|
||||||
|
updatedPost = updatedPost.copyWith(
|
||||||
|
likes: updatedPost.likes - 1,
|
||||||
|
likedBy: updatedPost.likedBy?..remove(userId),
|
||||||
|
);
|
||||||
|
await postCollection.doc(postId).update(updatedPost.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> unlikePostReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reaction: post.reaction - 1,
|
||||||
|
reactions: post.reactions
|
||||||
|
?..[post.reactions!
|
||||||
|
.indexWhere((element) => element.id == reaction.id)] =
|
||||||
|
reaction.copyWith(
|
||||||
|
likedBy: reaction.likedBy?..remove(userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await postCollection.doc(post.id).update(updatedPost.toJson());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import "package:cloud_firestore/cloud_firestore.dart";
|
||||||
|
import "package:firebase_auth/firebase_auth.dart";
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class FirebaseUserRepository implements TimelineUserRepositoryInterface {
|
||||||
|
final CollectionReference usersCollection =
|
||||||
|
FirebaseFirestore.instance.collection("users");
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<TimelineUser>> getAllUsers() async {
|
||||||
|
var users = await usersCollection
|
||||||
|
.withConverter<TimelineUser>(
|
||||||
|
fromFirestore: (snapshot, _) =>
|
||||||
|
TimelineUser.fromJson(snapshot.data()!, snapshot.id),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
return users.docs.map((e) => e.data()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelineUser> getCurrentUser() async {
|
||||||
|
var authUser = FirebaseAuth.instance.currentUser;
|
||||||
|
var user = await usersCollection
|
||||||
|
.doc(authUser!.uid)
|
||||||
|
.withConverter<TimelineUser>(
|
||||||
|
fromFirestore: (snapshot, _) =>
|
||||||
|
TimelineUser.fromJson(snapshot.data()!, snapshot.id),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
return user.data()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelineUser?> getUser(String userId) async {
|
||||||
|
var userDoc = await usersCollection
|
||||||
|
.doc(userId)
|
||||||
|
.withConverter<TimelineUser>(
|
||||||
|
fromFirestore: (snapshot, _) =>
|
||||||
|
TimelineUser.fromJson(snapshot.data()!, snapshot.id),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
// print(userDoc.data()?.firstName);
|
||||||
|
return userDoc.data();
|
||||||
|
}
|
||||||
|
}
|
29
packages/firebase_timeline_repository/pubspec.yaml
Normal file
29
packages/firebase_timeline_repository/pubspec.yaml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
name: firebase_timeline_repository
|
||||||
|
description: "A new Flutter package project."
|
||||||
|
version: 6.0.0
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.5.1
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
timeline_repository_interface:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
|
path: packages/timeline_repository_interface
|
||||||
|
ref: 6.0.0
|
||||||
|
|
||||||
|
rxdart: any
|
||||||
|
cloud_firestore: ^5.4.4
|
||||||
|
firebase_auth: ^5.3.1
|
||||||
|
firebase_storage: ^12.3.2
|
||||||
|
collection: ^1.18.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_iconica_analysis:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
|
ref: 7.0.0
|
36
packages/flutter_timeline/.gitignore
vendored
Normal file
36
packages/flutter_timeline/.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
|
|
||||||
|
ios
|
||||||
|
android
|
||||||
|
web
|
||||||
|
macos
|
||||||
|
windows
|
||||||
|
linux
|
1
packages/flutter_timeline/CONTRIBUTING.md
Symbolic link
1
packages/flutter_timeline/CONTRIBUTING.md
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../CONTRIBUTING.md
|
|
@ -1,13 +1,7 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
|
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 713 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
packages/flutter_timeline/example/fonts/Avenir-Regular.ttf
Normal file
BIN
packages/flutter_timeline/example/fonts/Avenir-Regular.ttf
Normal file
Binary file not shown.
BIN
packages/flutter_timeline/example/fonts/Merriweather-Regular.ttf
Normal file
BIN
packages/flutter_timeline/example/fonts/Merriweather-Regular.ttf
Normal file
Binary file not shown.
|
@ -1,45 +0,0 @@
|
||||||
import 'package:example/config/config.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
|
||||||
|
|
||||||
class NavigatorApp extends StatelessWidget {
|
|
||||||
const NavigatorApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'Flutter Timeline',
|
|
||||||
theme: ThemeData(
|
|
||||||
colorScheme:
|
|
||||||
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
|
||||||
surface: const Color(0xFFB8E2E8),
|
|
||||||
),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: const MyHomePage(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
|
||||||
const MyHomePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
var timelineService =
|
|
||||||
TimelineService(postService: LocalTimelinePostService());
|
|
||||||
var timelineOptions = options;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return timeLineNavigatorUserStory(
|
|
||||||
context: context,
|
|
||||||
configuration: getConfig(timelineService),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
import 'package:example/config/config.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
|
||||||
|
|
||||||
class WidgetApp extends StatelessWidget {
|
|
||||||
const WidgetApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'Flutter Timeline',
|
|
||||||
theme: ThemeData(
|
|
||||||
colorScheme:
|
|
||||||
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
|
||||||
surface: const Color(0xFFB8E2E8),
|
|
||||||
),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: const MyHomePage(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
|
||||||
const MyHomePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
var timelineService =
|
|
||||||
TimelineService(postService: LocalTimelinePostService());
|
|
||||||
var timelineOptions = options;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
floatingActionButton: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
FloatingActionButton(
|
|
||||||
heroTag: 'btn1',
|
|
||||||
onPressed: () {
|
|
||||||
createPost(
|
|
||||||
context,
|
|
||||||
timelineService,
|
|
||||||
timelineOptions,
|
|
||||||
getConfig(timelineService),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Icon(
|
|
||||||
Icons.edit,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
FloatingActionButton(
|
|
||||||
heroTag: 'btn2',
|
|
||||||
onPressed: () {
|
|
||||||
generatePost(timelineService);
|
|
||||||
},
|
|
||||||
child: const Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: const SafeArea(
|
|
||||||
child: TimelineScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
|
||||||
|
|
||||||
class PostScreen extends StatefulWidget {
|
|
||||||
const PostScreen({
|
|
||||||
required this.service,
|
|
||||||
required this.post,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TimelineService service;
|
|
||||||
final TimelinePost post;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PostScreen> createState() => _PostScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PostScreenState extends State<PostScreen> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: TimelinePostScreen(
|
|
||||||
userId: 'test_user',
|
|
||||||
service: widget.service,
|
|
||||||
options: const TimelineOptions(),
|
|
||||||
post: widget.post,
|
|
||||||
onPostDelete: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestUserService implements TimelineUserService {
|
|
||||||
final Map<String, TimelinePosterUserModel> _users = {
|
|
||||||
'test_user': 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',
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePosterUserModel?> getUser(String userId) async {
|
|
||||||
if (_users.containsKey(userId)) {
|
|
||||||
return _users[userId]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
_users[userId] = TimelinePosterUserModel(userId: userId);
|
|
||||||
|
|
||||||
return TimelinePosterUserModel(userId: userId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
|
||||||
|
|
||||||
TimelineUserStoryConfiguration getConfig(TimelineService service) {
|
|
||||||
return TimelineUserStoryConfiguration(
|
|
||||||
service: service,
|
|
||||||
userId: 'test_user',
|
|
||||||
optionsBuilder: (context) => options,
|
|
||||||
enablePostOverviewScreen: false,
|
|
||||||
canDeleteAllPosts: (_) => true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = TimelineOptions(
|
|
||||||
textInputBuilder: null,
|
|
||||||
paddings: TimelinePaddingOptions(
|
|
||||||
mainPadding: const EdgeInsets.all(20).copyWith(top: 28),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
void navigateToOverview(
|
|
||||||
BuildContext context,
|
|
||||||
TimelineService service,
|
|
||||||
TimelineOptions options,
|
|
||||||
TimelinePost post,
|
|
||||||
) {
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => TimelinePostOverviewScreen(
|
|
||||||
timelinePost: post,
|
|
||||||
options: options,
|
|
||||||
service: service,
|
|
||||||
onPostSubmit: (post) {
|
|
||||||
service.postService.createPost(post);
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void createPost(
|
|
||||||
BuildContext context,
|
|
||||||
TimelineService service,
|
|
||||||
TimelineOptions options,
|
|
||||||
TimelineUserStoryConfiguration configuration) async {
|
|
||||||
await Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => Scaffold(
|
|
||||||
body: TimelinePostCreationScreen(
|
|
||||||
postCategory: 'category1',
|
|
||||||
userId: 'test_user',
|
|
||||||
service: service,
|
|
||||||
options: options,
|
|
||||||
onPostCreated: (post) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
onPostOverview: (post) {
|
|
||||||
navigateToOverview(context, service, options, post);
|
|
||||||
},
|
|
||||||
enablePostOverviewScreen: configuration.enablePostOverviewScreen,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void generatePost(TimelineService service) {
|
|
||||||
var amountOfPosts = service.postService.getPosts(null).length;
|
|
||||||
|
|
||||||
service.postService.createPost(
|
|
||||||
TimelinePost(
|
|
||||||
id: 'Post$amountOfPosts',
|
|
||||||
creatorId: 'test_user',
|
|
||||||
title: 'Post $amountOfPosts',
|
|
||||||
category: amountOfPosts % 2 == 0 ? 'category1' : 'category2',
|
|
||||||
content: "Post $amountOfPosts content",
|
|
||||||
likes: 0,
|
|
||||||
reaction: 0,
|
|
||||||
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',
|
|
||||||
),
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
reactionEnabled: amountOfPosts % 2 == 0 ? false : true,
|
|
||||||
imageUrl: amountOfPosts % 3 != 0
|
|
||||||
? 'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2'
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,26 @@
|
||||||
import 'package:example/apps/navigator/app.dart';
|
import 'package:example/theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_timeline/flutter_timeline.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
|
||||||
void main() {
|
void main(List<String> args) {
|
||||||
initializeDateFormatting();
|
initializeDateFormatting();
|
||||||
|
|
||||||
runApp(const NavigatorApp());
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
var timelineService = TimelineService();
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: theme,
|
||||||
|
home: const FlutterTimelineNavigatorUserstory(
|
||||||
|
currentUserId: "1",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
147
packages/flutter_timeline/example/lib/theme.dart
Normal file
147
packages/flutter_timeline/example/lib/theme.dart
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const Color primaryColor = Color(0xFF71C6D1);
|
||||||
|
|
||||||
|
ThemeData theme = ThemeData(
|
||||||
|
actionIconTheme: ActionIconThemeData(
|
||||||
|
backButtonIconBuilder: (context) {
|
||||||
|
return const Icon(Icons.arrow_back_ios_new_rounded);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: const Color(0xFFFAF9F6),
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
checkboxTheme: CheckboxThemeData(
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color(0xFF8D8D8D),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
fillColor: WidgetStateProperty.resolveWith<Color>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
return const Color(0xFFEEEEEE);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
switchTheme: SwitchThemeData(
|
||||||
|
trackColor:
|
||||||
|
WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
|
||||||
|
if (!states.contains(WidgetState.selected)) {
|
||||||
|
return const Color(0xFF8D8D8D);
|
||||||
|
}
|
||||||
|
return primaryColor;
|
||||||
|
}),
|
||||||
|
thumbColor: const WidgetStatePropertyAll(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
centerTitle: true,
|
||||||
|
iconTheme: IconThemeData(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Color(0xFF212121),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 24,
|
||||||
|
color: Color(0xFF71C6D1),
|
||||||
|
fontFamily: "Merriweather",
|
||||||
|
),
|
||||||
|
actionsIconTheme: IconThemeData()),
|
||||||
|
fontFamily: "Merriweather",
|
||||||
|
useMaterial3: false,
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
headlineSmall: TextStyle(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
headlineLarge: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 24,
|
||||||
|
color: Color(0xFF71C6D1),
|
||||||
|
),
|
||||||
|
|
||||||
|
displayLarge: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
displayMedium: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 18,
|
||||||
|
color: Color(0xFF71C6D1),
|
||||||
|
),
|
||||||
|
displaySmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
|
||||||
|
// TITLE
|
||||||
|
titleSmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
|
||||||
|
// LABEL
|
||||||
|
labelSmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF8D8D8D),
|
||||||
|
),
|
||||||
|
|
||||||
|
// BODY
|
||||||
|
bodySmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
bodyMedium: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
radioTheme: RadioThemeData(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: 0,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
fillColor: WidgetStateProperty.resolveWith<Color>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
return Colors.black;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: primaryColor,
|
||||||
|
),
|
||||||
|
);
|
|
@ -1,92 +1,28 @@
|
||||||
name: example
|
name: example
|
||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
# The following line prevents the package from being accidentally published to
|
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
publish_to: "none"
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.2.3 <4.0.0'
|
sdk: ^3.5.1
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
|
||||||
cupertino_icons: ^1.0.2
|
|
||||||
flutter_timeline:
|
flutter_timeline:
|
||||||
path: ../
|
path: ../
|
||||||
intl: ^0.19.0
|
intl: 0.19.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_lints: ^4.0.0
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
|
||||||
flutter_lints: ^2.0.0
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
fonts:
|
||||||
# To add assets to your application, add an assets section, like this:
|
- family: Merriweather
|
||||||
# assets:
|
fonts:
|
||||||
# - assets/
|
- asset: fonts/Merriweather-Regular.ttf
|
||||||
|
- family: Avenir
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
fonts:
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
- asset: fonts/Avenir-Regular.ttf
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:example/apps/widgets/app.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(const WidgetApp());
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,12 +1,30 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
// ignore_for_file: directives_ordering
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
/// Flutter Timeline library
|
/// userstory
|
||||||
library flutter_timeline;
|
library;
|
||||||
|
|
||||||
export 'package:flutter_timeline/src/flutter_timeline_navigator_userstory.dart';
|
export "src/flutter_timeline_navigator_userstory.dart";
|
||||||
export 'package:flutter_timeline/src/models/timeline_configuration.dart';
|
export "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
export 'package:flutter_timeline/src/routes.dart';
|
|
||||||
export 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
/// models
|
||||||
export 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
export "src/models/timeline_options.dart";
|
||||||
|
export "src/models/timeline_translations.dart";
|
||||||
|
|
||||||
|
/// screens
|
||||||
|
export "src/screens/timeline_screen.dart";
|
||||||
|
export "src/screens/timeline_post_overview.dart";
|
||||||
|
export "src/screens/timeline_post_detail_screen.dart";
|
||||||
|
export "src/screens/timeline_add_post_information_screen.dart";
|
||||||
|
export "src/screens/timeline_choose_category_screen.dart";
|
||||||
|
|
||||||
|
/// widgets
|
||||||
|
export "src/widgets/category_list.dart";
|
||||||
|
export "src/widgets/category_widget.dart";
|
||||||
|
export "src/widgets/comment_section.dart";
|
||||||
|
export "src/widgets/image_picker.dart";
|
||||||
|
export "src/widgets/post_info_textfield.dart";
|
||||||
|
export "src/widgets/post_list.dart";
|
||||||
|
export "src/widgets/post_more_options_widget.dart";
|
||||||
|
export "src/widgets/reaction_textfield.dart";
|
||||||
|
export "src/widgets/tappable_image.dart";
|
||||||
|
export "src/widgets/timeline_post.dart";
|
||||||
|
|
|
@ -1,374 +1,115 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Iconica
|
import "package:flutter/material.dart";
|
||||||
//
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
class FlutterTimelineNavigatorUserstory extends StatefulWidget {
|
||||||
import 'package:flutter_timeline/flutter_timeline.dart';
|
const FlutterTimelineNavigatorUserstory({
|
||||||
|
required this.currentUserId,
|
||||||
|
this.options = const TimelineOptions(),
|
||||||
|
this.timelineService,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
/// A widget function that creates a timeline navigator for user stories.
|
final TimelineOptions options;
|
||||||
///
|
final TimelineService? timelineService;
|
||||||
/// This function creates a navigator for displaying user stories on a timeline.
|
final String currentUserId;
|
||||||
/// It takes a [BuildContext] and an optional [TimelineUserStoryConfiguration]
|
|
||||||
/// as parameters. If no configuration is provided, default values will be used.
|
|
||||||
late TimelineUserStoryConfiguration timelineUserStoryConfiguration;
|
|
||||||
|
|
||||||
Widget timeLineNavigatorUserStory({
|
@override
|
||||||
required BuildContext context,
|
State<FlutterTimelineNavigatorUserstory> createState() =>
|
||||||
TimelineUserStoryConfiguration? configuration,
|
_FlutterTimelineNavigatorUserstoryState();
|
||||||
}) {
|
|
||||||
timelineUserStoryConfiguration = configuration ??
|
|
||||||
TimelineUserStoryConfiguration(
|
|
||||||
userId: 'test_user',
|
|
||||||
service: TimelineService(
|
|
||||||
postService: LocalTimelinePostService(),
|
|
||||||
),
|
|
||||||
optionsBuilder: (context) => const TimelineOptions(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return _timelineScreenRoute(
|
|
||||||
config: timelineUserStoryConfiguration,
|
|
||||||
context: context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget function that creates a timeline screen route.
|
class _FlutterTimelineNavigatorUserstoryState
|
||||||
///
|
extends State<FlutterTimelineNavigatorUserstory> {
|
||||||
/// This function creates a route for displaying a timeline screen. It takes
|
late TimelineService timelineService;
|
||||||
/// a [BuildContext] and an optional [TimelineUserStoryConfiguration] as
|
|
||||||
/// parameters. If no configuration is provided, default values will be used.
|
|
||||||
Widget _timelineScreenRoute({
|
|
||||||
required BuildContext context,
|
|
||||||
required TimelineUserStoryConfiguration config,
|
|
||||||
String? initalCategory,
|
|
||||||
}) {
|
|
||||||
var timelineScreen = TimelineScreen(
|
|
||||||
timelineCategory: initalCategory,
|
|
||||||
userId: config.getUserId?.call(context) ?? config.userId,
|
|
||||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
|
||||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
|
||||||
service: config.service,
|
|
||||||
options: config.optionsBuilder(context),
|
|
||||||
onPostTap: (post) async =>
|
|
||||||
config.onPostTap?.call(context, post) ??
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _postDetailScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
post: post,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onRefresh: config.onRefresh,
|
|
||||||
filterEnabled: config.filterEnabled,
|
|
||||||
postWidgetBuilder: config.postWidgetBuilder,
|
|
||||||
);
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var button = FloatingActionButton(
|
|
||||||
backgroundColor: config
|
|
||||||
.optionsBuilder(context)
|
|
||||||
.theme
|
|
||||||
.postCreationFloatingActionButtonColor ??
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
onPressed: () async {
|
|
||||||
var selectedCategory = config.service.postService.selectedCategory;
|
|
||||||
if (selectedCategory != null && selectedCategory.key != null) {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _postCreationScreenRoute(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
category: selectedCategory,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _postCategorySelectionScreen(
|
|
||||||
config: config,
|
|
||||||
context: context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return config.homeOpenPageBuilder?.call(context, timelineScreen, button) ??
|
@override
|
||||||
Scaffold(
|
void initState() {
|
||||||
appBar: AppBar(
|
timelineService = widget.timelineService ?? TimelineService();
|
||||||
title: Text(
|
super.initState();
|
||||||
config.optionsBuilder(context).translations.timeLineScreenTitle,
|
}
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: timelineScreen,
|
|
||||||
floatingActionButton: button,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget function that creates a post detail screen route.
|
@override
|
||||||
///
|
Widget build(BuildContext context) => _timelineScreenWidget();
|
||||||
/// This function creates a route for displaying a post detail screen. It takes
|
|
||||||
/// a [BuildContext], a [TimelinePost], and an optional
|
Widget _timelineScreenWidget() => TimelineScreen(
|
||||||
/// [TimelineUserStoryConfiguration] as parameters. If no configuration is
|
currentUserId: widget.currentUserId,
|
||||||
/// provided, default values will be used.
|
timelineService: timelineService,
|
||||||
Widget _postDetailScreenRoute({
|
options: widget.options,
|
||||||
required BuildContext context,
|
onTapComments: (post) async {
|
||||||
required TimelinePost post,
|
var currentUser = await timelineService.getCurrentUser();
|
||||||
required TimelineUserStoryConfiguration config,
|
|
||||||
}) {
|
await widget.options.onTapComments?.call(post, currentUser) ??
|
||||||
var timelinePostScreen = TimelinePostScreen(
|
await _push(_timelinePostDetailScreenWidget(post, currentUser));
|
||||||
userId: config.getUserId?.call(context) ?? config.userId,
|
},
|
||||||
allowAllDeletion: config.canDeleteAllPosts?.call(context) ?? false,
|
onTapCreatePost: () async {
|
||||||
options: config.optionsBuilder(context),
|
var selectedCategory = timelineService.getSelectedCategory();
|
||||||
service: config.service,
|
if (widget.options.onTapCreatePost != null) {
|
||||||
post: post,
|
await widget.options.onTapCreatePost!(selectedCategory);
|
||||||
onPostDelete: () async =>
|
} else {
|
||||||
config.onPostDelete?.call(context, post) ??
|
if (selectedCategory?.key != null) {
|
||||||
() async {
|
await _push(_timelineAddpostInformationScreenWidget());
|
||||||
await config.service.postService.deletePost(post);
|
} else {
|
||||||
if (context.mounted) {
|
await _push(_timelineChooseCategoryScreenWidget());
|
||||||
Navigator.of(context).pop();
|
}
|
||||||
}
|
}
|
||||||
}.call(),
|
},
|
||||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
onTapPost: (post) async {
|
||||||
);
|
var currentUser = await timelineService.getCurrentUser();
|
||||||
|
if (context.mounted)
|
||||||
var category = config.service.postService.categories
|
await widget.options.onTapPost?.call(post, currentUser) ??
|
||||||
.firstWhere((element) => element.key == post.category);
|
await _push(_timelinePostDetailScreenWidget(post, currentUser));
|
||||||
|
},
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget function that creates a post creation screen route.
|
Widget _timelinePostDetailScreenWidget(
|
||||||
///
|
TimelinePost post,
|
||||||
/// This function creates a route for displaying a post creation screen.
|
TimelineUser currentUser,
|
||||||
/// It takes a [BuildContext] and an optional [TimelineUserStoryConfiguration]
|
) =>
|
||||||
/// as parameters. If no configuration is provided, default values will be used.
|
TimelinePostDetailScreen(
|
||||||
Widget _postCreationScreenRoute({
|
currentUserId: widget.currentUserId,
|
||||||
required BuildContext context,
|
timelineService: timelineService,
|
||||||
required TimelineCategory category,
|
currentUser: currentUser,
|
||||||
required TimelineUserStoryConfiguration config,
|
options: widget.options,
|
||||||
}) {
|
|
||||||
var timelinePostCreationScreen = TimelinePostCreationScreen(
|
|
||||||
userId: config.getUserId?.call(context) ?? config.userId,
|
|
||||||
options: config.optionsBuilder(context),
|
|
||||||
service: config.service,
|
|
||||||
onPostCreated: (post) async {
|
|
||||||
var newPost = await config.service.postService.createPost(post);
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
|
||||||
if (config.afterPostCreationGoHome) {
|
|
||||||
await Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _timelineScreenRoute(
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enablePostOverviewScreen: config.enablePostOverviewScreen,
|
|
||||||
postCategory: category.key,
|
|
||||||
);
|
|
||||||
|
|
||||||
var backButton = IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return config.postCreationOpenPageBuilder
|
|
||||||
?.call(context, timelinePostCreationScreen, backButton) ??
|
|
||||||
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: timelinePostCreationScreen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget function that creates a post overview screen route.
|
|
||||||
///
|
|
||||||
/// This function creates a route for displaying a post overview screen.
|
|
||||||
/// It takes a [BuildContext], a [TimelinePost], and an optional
|
|
||||||
/// [TimelineUserStoryConfiguration] as parameters. If no configuration is
|
|
||||||
/// provided, default values will be used.
|
|
||||||
Widget _postOverviewScreenRoute({
|
|
||||||
required BuildContext context,
|
|
||||||
required TimelinePost post,
|
|
||||||
required TimelineUserStoryConfiguration config,
|
|
||||||
}) {
|
|
||||||
var timelinePostOverviewWidget = TimelinePostOverviewScreen(
|
|
||||||
options: config.optionsBuilder(context),
|
|
||||||
service: config.service,
|
|
||||||
timelinePost: post,
|
|
||||||
onPostSubmit: (post) async {
|
|
||||||
var createdPost = await config.service.postService.createPost(post);
|
|
||||||
config.onPostCreate?.call(createdPost);
|
|
||||||
if (context.mounted) {
|
|
||||||
await Navigator.of(context).pushAndRemoveUntil(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _timelineScreenRoute(
|
|
||||||
config: config,
|
|
||||||
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,
|
post: post,
|
||||||
),
|
);
|
||||||
),
|
|
||||||
);
|
Widget _timelineChooseCategoryScreenWidget() => TimelineChooseCategoryScreen(
|
||||||
|
timelineService: timelineService,
|
||||||
|
options: widget.options,
|
||||||
|
ontapCategory: (category) async {
|
||||||
|
await widget.options.onTapCategory?.call(category) ??
|
||||||
|
await _push(_timelineAddpostInformationScreenWidget());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _timelineAddpostInformationScreenWidget() =>
|
||||||
|
TimelineAddPostInformationScreen(
|
||||||
|
timelineService: timelineService,
|
||||||
|
options: widget.options,
|
||||||
|
onTapOverview: () async {
|
||||||
|
await widget.options.onTapOverview?.call() ??
|
||||||
|
await _push(_timelinePostOverviewWidget());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _timelinePostOverviewWidget() => TimelinePostOverview(
|
||||||
|
timelineService: timelineService,
|
||||||
|
options: widget.options,
|
||||||
|
onTapCreatePost: (post) async {
|
||||||
|
await widget.options.onTapCreatePostInOverview?.call(post) ??
|
||||||
|
await _pushAndRemoveUntil(_timelineScreenWidget());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> _push(Widget screen) async {
|
||||||
|
await Navigator.of(context)
|
||||||
|
.push(MaterialPageRoute(builder: (context) => screen));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pushAndRemoveUntil(Widget screen) async {
|
||||||
|
await Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (context) => screen),
|
||||||
|
(route) => route.isFirst,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
|
||||||
|
|
||||||
/// Configuration class for defining user-specific settings and callbacks for a
|
|
||||||
/// timeline user story.
|
|
||||||
///
|
|
||||||
/// This class holds various parameters to customize the behavior and appearance
|
|
||||||
/// of a user story timeline.
|
|
||||||
@immutable
|
|
||||||
class TimelineUserStoryConfiguration {
|
|
||||||
/// Constructs a [TimelineUserStoryConfiguration] with the specified
|
|
||||||
/// parameters.
|
|
||||||
///
|
|
||||||
/// [service] is the TimelineService responsible for fetching user story data.
|
|
||||||
///
|
|
||||||
/// [optionsBuilder] is a function that builds TimelineOptions based on the
|
|
||||||
/// given [BuildContext].
|
|
||||||
///
|
|
||||||
/// [userId] is the ID of the user associated with this user story
|
|
||||||
/// configuration. Default is 'test_user'.
|
|
||||||
///
|
|
||||||
/// [openPageBuilder] is a function that defines the behavior when a page
|
|
||||||
/// needs to be opened. This function should accept a [BuildContext] and a
|
|
||||||
/// child widget.
|
|
||||||
///
|
|
||||||
/// [onPostTap] is a callback function invoked when a timeline post is
|
|
||||||
/// tapped. It should accept a [BuildContext] and the tapped post.
|
|
||||||
///
|
|
||||||
/// [onUserTap] is a callback function invoked when the user's profile is
|
|
||||||
/// tapped. It should accept a [BuildContext] and the user ID of the tapped
|
|
||||||
/// user.
|
|
||||||
///
|
|
||||||
/// [onPostDelete] is a callback function invoked when a post deletion is
|
|
||||||
/// requested. It should accept a [BuildContext] and the post widget. This
|
|
||||||
/// function can return a widget to be displayed after the post is deleted.
|
|
||||||
///
|
|
||||||
/// [filterEnabled] determines whether filtering functionality is enabled for
|
|
||||||
/// this user story timeline. Default is false.
|
|
||||||
///
|
|
||||||
/// [postWidgetBuilder] is a function that builds a widget for a timeline
|
|
||||||
/// post. It should accept a [TimelinePost] and return a widget representing
|
|
||||||
/// that post.
|
|
||||||
const TimelineUserStoryConfiguration({
|
|
||||||
required this.service,
|
|
||||||
required this.optionsBuilder,
|
|
||||||
this.getUserId,
|
|
||||||
this.serviceBuilder,
|
|
||||||
this.canDeleteAllPosts,
|
|
||||||
this.userId = 'test_user',
|
|
||||||
this.homeOpenPageBuilder,
|
|
||||||
this.postCreationOpenPageBuilder,
|
|
||||||
this.postViewOpenPageBuilder,
|
|
||||||
this.postOverviewOpenPageBuilder,
|
|
||||||
this.onPostTap,
|
|
||||||
this.onUserTap,
|
|
||||||
this.onRefresh,
|
|
||||||
this.onPostDelete,
|
|
||||||
this.filterEnabled = false,
|
|
||||||
this.postWidgetBuilder,
|
|
||||||
this.afterPostCreationGoHome = false,
|
|
||||||
this.enablePostOverviewScreen = true,
|
|
||||||
this.categorySelectionOpenPageBuilder,
|
|
||||||
this.onPostCreate,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The ID of the user associated with this user story configuration.
|
|
||||||
final String userId;
|
|
||||||
|
|
||||||
/// A function to get the userId only when needed and with a context
|
|
||||||
final String Function(BuildContext context)? getUserId;
|
|
||||||
|
|
||||||
/// A function to determine if a user can delete posts that is called
|
|
||||||
/// when needed
|
|
||||||
final bool Function(BuildContext context)? canDeleteAllPosts;
|
|
||||||
|
|
||||||
/// The TimelineService responsible for fetching user story data.
|
|
||||||
final TimelineService service;
|
|
||||||
|
|
||||||
/// A function to get the timeline service only when needed and with a context
|
|
||||||
final TimelineService Function(BuildContext context)? serviceBuilder;
|
|
||||||
|
|
||||||
/// A function that builds TimelineOptions based on the given BuildContext.
|
|
||||||
final TimelineOptions Function(BuildContext context) optionsBuilder;
|
|
||||||
|
|
||||||
/// Open page builder function for the home page. This function accepts
|
|
||||||
/// a [BuildContext], a child widget, and a FloatingActionButton which can
|
|
||||||
/// route to the post creation page.
|
|
||||||
|
|
||||||
final Function(
|
|
||||||
BuildContext context,
|
|
||||||
Widget child,
|
|
||||||
FloatingActionButton? button,
|
|
||||||
)? homeOpenPageBuilder;
|
|
||||||
|
|
||||||
/// Open page builder function for the post creation page. This function
|
|
||||||
/// accepts a [BuildContext], a child widget, and an IconButton which can
|
|
||||||
/// route to the home page.
|
|
||||||
|
|
||||||
final Function(
|
|
||||||
BuildContext context,
|
|
||||||
Widget child,
|
|
||||||
IconButton? button,
|
|
||||||
)? postCreationOpenPageBuilder;
|
|
||||||
|
|
||||||
/// Open page builder function for the post view page. This function accepts
|
|
||||||
/// a [BuildContext], a child widget, and an IconButton which can route to the
|
|
||||||
/// home page.
|
|
||||||
|
|
||||||
final Function(
|
|
||||||
BuildContext context,
|
|
||||||
Widget child,
|
|
||||||
IconButton? button,
|
|
||||||
TimelinePost post,
|
|
||||||
TimelineCategory? category,
|
|
||||||
)? postViewOpenPageBuilder;
|
|
||||||
|
|
||||||
/// Open page builder function for the post overview page. This function
|
|
||||||
/// accepts a [BuildContext], a child widget, and an IconButton which can
|
|
||||||
/// route to the home page.
|
|
||||||
|
|
||||||
final Function(
|
|
||||||
BuildContext context,
|
|
||||||
Widget child,
|
|
||||||
)? postOverviewOpenPageBuilder;
|
|
||||||
|
|
||||||
/// A callback function invoked when a timeline post is tapped.
|
|
||||||
final Function(BuildContext context, TimelinePost post)? onPostTap;
|
|
||||||
|
|
||||||
/// A callback function invoked when the user's profile is tapped.
|
|
||||||
final Function(BuildContext context, String userId)? onUserTap;
|
|
||||||
|
|
||||||
/// A callback function invoked when the timeline is refreshed by pulling down
|
|
||||||
final Function(BuildContext context, String? category)? onRefresh;
|
|
||||||
|
|
||||||
/// A callback function invoked when a post deletion is requested.
|
|
||||||
final Widget Function(BuildContext context, TimelinePost post)? onPostDelete;
|
|
||||||
|
|
||||||
/// Determines whether filtering functionality is enabled for this user story
|
|
||||||
/// timeline.
|
|
||||||
final bool filterEnabled;
|
|
||||||
|
|
||||||
/// A function that builds a widget for a timeline post.
|
|
||||||
final Widget Function(TimelinePost post)? postWidgetBuilder;
|
|
||||||
|
|
||||||
/// Boolean to enable timeline post overview screen before submitting
|
|
||||||
final bool enablePostOverviewScreen;
|
|
||||||
|
|
||||||
/// Boolean to enable redirect to home after post creation.
|
|
||||||
/// If false, it will redirect to created post screen
|
|
||||||
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;
|
|
||||||
}
|
|
190
packages/flutter_timeline/lib/src/models/timeline_options.dart
Normal file
190
packages/flutter_timeline/lib/src/models/timeline_options.dart
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_image_picker/flutter_image_picker.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
import "package:intl/intl.dart";
|
||||||
|
|
||||||
|
class TimelineOptions {
|
||||||
|
const TimelineOptions({
|
||||||
|
this.translations = const TimelineTranslations(),
|
||||||
|
this.everyoneCanDelete = false,
|
||||||
|
this.onTapLike,
|
||||||
|
this.onTapUnlike,
|
||||||
|
this.onPostDelete,
|
||||||
|
this.userAvatarBuilder = _defaultUserAvatarBuilder,
|
||||||
|
this.iconSize = 24,
|
||||||
|
this.iconColor = Colors.black,
|
||||||
|
this.doubleTapToLike = false,
|
||||||
|
this.userNameBuilder = _defaultNameBuilder,
|
||||||
|
this.floatingActionButtonBuilder = _defaultFloatingActionButton,
|
||||||
|
this.allowCreatingCategories = true,
|
||||||
|
this.initialCategoryId,
|
||||||
|
this.likeIcon = Icons.favorite_outline,
|
||||||
|
this.likedIcon = Icons.favorite,
|
||||||
|
this.commentIcon = Icons.chat_bubble_outline,
|
||||||
|
this.imagePickerTheme,
|
||||||
|
this.dateFormat = _defaultDateFormat,
|
||||||
|
this.buttonBuilder = _defaultButtonBuilder,
|
||||||
|
this.postBuilder,
|
||||||
|
this.timelineScreenDrawer,
|
||||||
|
this.timelineScreenAppBarBuilder,
|
||||||
|
this.onCreatePost,
|
||||||
|
this.onTapComments,
|
||||||
|
this.onTapCreatePost,
|
||||||
|
this.onTapPost,
|
||||||
|
this.onTapCategory,
|
||||||
|
this.onTapOverview,
|
||||||
|
this.onTapCreatePostInOverview,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Builders
|
||||||
|
final UserAvatarBuilder userAvatarBuilder;
|
||||||
|
final UserNameBuilder userNameBuilder;
|
||||||
|
final DateFormat Function(BuildContext context) dateFormat;
|
||||||
|
final FloatingActionButtonBuilder floatingActionButtonBuilder;
|
||||||
|
final ButtonBuilder buttonBuilder;
|
||||||
|
final PostBuilder? postBuilder;
|
||||||
|
|
||||||
|
//general
|
||||||
|
final TimelineTranslations translations;
|
||||||
|
final Function(TimelinePost post, TimelineUser user)? onTapComments;
|
||||||
|
final Function(TimelineCategory? category)? onTapCreatePost;
|
||||||
|
final Function(TimelinePost post, TimelineUser user)? onTapPost;
|
||||||
|
final Function(TimelineCategory? category)? onTapCategory;
|
||||||
|
final Function()? onTapOverview;
|
||||||
|
final Function(TimelinePost post)? onTapCreatePostInOverview;
|
||||||
|
|
||||||
|
// TimelinePostWidget
|
||||||
|
final bool everyoneCanDelete;
|
||||||
|
final VoidCallback? onTapLike;
|
||||||
|
final VoidCallback? onTapUnlike;
|
||||||
|
final VoidCallback? onPostDelete;
|
||||||
|
final Function(TimelinePost post)? onCreatePost;
|
||||||
|
final double iconSize;
|
||||||
|
final Color iconColor;
|
||||||
|
final IconData likeIcon;
|
||||||
|
final IconData likedIcon;
|
||||||
|
final IconData commentIcon;
|
||||||
|
final bool doubleTapToLike;
|
||||||
|
final bool allowCreatingCategories;
|
||||||
|
final String? initialCategoryId;
|
||||||
|
final ImagePickerTheme? imagePickerTheme;
|
||||||
|
final Widget? timelineScreenDrawer;
|
||||||
|
final AppBarBuilder? timelineScreenAppBarBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _defaultFloatingActionButton(
|
||||||
|
Function() onPressed,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return FloatingActionButton.large(
|
||||||
|
backgroundColor: theme.primaryColor,
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Icon(
|
||||||
|
Icons.add,
|
||||||
|
size: 44,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _defaultUserAvatarBuilder(TimelineUser? user, double size) {
|
||||||
|
if (user == null || user.imageUrl == null) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: size / 2,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.person,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
image: DecorationImage(
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
user.imageUrl!,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _defaultNameBuilder(
|
||||||
|
TimelineUser? user,
|
||||||
|
String anonymousUserText,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
if (user == null || user.fullName == null) {
|
||||||
|
return Text(anonymousUserText);
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
user.fullName!,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _defaultButtonBuilder({
|
||||||
|
required String title,
|
||||||
|
required Function() onPressed,
|
||||||
|
required BuildContext context,
|
||||||
|
}) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
|
child: FilledButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
maximumSize: const Size(254, 50),
|
||||||
|
minimumSize: const Size(254, 50),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateFormat _defaultDateFormat(BuildContext context) => DateFormat(
|
||||||
|
"dd/MM/yyyy 'at' HH:mm",
|
||||||
|
Localizations.localeOf(context).languageCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef UserAvatarBuilder = Widget Function(
|
||||||
|
TimelineUser? user,
|
||||||
|
double size,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef UserNameBuilder = Widget Function(
|
||||||
|
TimelineUser? user,
|
||||||
|
String anonymousUserText,
|
||||||
|
BuildContext context,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef FloatingActionButtonBuilder = Widget Function(
|
||||||
|
Function() onPressed,
|
||||||
|
BuildContext context,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef ButtonBuilder = Widget Function({
|
||||||
|
required String title,
|
||||||
|
required Function() onPressed,
|
||||||
|
required BuildContext context,
|
||||||
|
});
|
||||||
|
|
||||||
|
typedef PostBuilder = Widget Function({
|
||||||
|
required TimelinePost post,
|
||||||
|
required Function(TimelinePost) onTap,
|
||||||
|
required BuildContext context,
|
||||||
|
});
|
||||||
|
|
||||||
|
typedef AppBarBuilder = PreferredSizeWidget Function(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
);
|
|
@ -0,0 +1,58 @@
|
||||||
|
class TimelineTranslations {
|
||||||
|
const TimelineTranslations({
|
||||||
|
this.timelineTitle = "iconinstagram",
|
||||||
|
this.viewPostTitle = "View post",
|
||||||
|
this.deletePostTitle = "Delete",
|
||||||
|
this.oneLikeTitle = "like",
|
||||||
|
this.multipleLikesTitle = "likes",
|
||||||
|
this.anonymousUser = "Anonymous User",
|
||||||
|
this.commentFieldHint = "Write your comment here...",
|
||||||
|
this.commentsTitle = "Comments",
|
||||||
|
this.allowCommentsYes = "Yes",
|
||||||
|
this.allowCommentsNo = "No",
|
||||||
|
this.allowCommentsTitle = "Are people allowed to comment?",
|
||||||
|
this.allowCommentsDescription =
|
||||||
|
"Indicate whether people are allowed to respond",
|
||||||
|
this.uploadimageTitle = "Upload image",
|
||||||
|
this.uploadImageDescription = "Upload an image to your message",
|
||||||
|
this.postTitle = "Title",
|
||||||
|
this.postTitleHint = "Title...",
|
||||||
|
this.contentTitle = "Content",
|
||||||
|
this.contentDescription = "What do you want to share?",
|
||||||
|
this.contentTitleHint = "Content...",
|
||||||
|
this.titleErrorText = "Please enter a title",
|
||||||
|
this.contentErrorText = "Please enter content",
|
||||||
|
this.addPost = "Add post",
|
||||||
|
this.overviewButton = "Overview",
|
||||||
|
this.chooseCategory = "Choose a category",
|
||||||
|
this.addCategory = "Add category",
|
||||||
|
this.postButtonTitle = "Post",
|
||||||
|
});
|
||||||
|
|
||||||
|
final String timelineTitle;
|
||||||
|
final String viewPostTitle;
|
||||||
|
final String deletePostTitle;
|
||||||
|
final String oneLikeTitle;
|
||||||
|
final String multipleLikesTitle;
|
||||||
|
final String anonymousUser;
|
||||||
|
final String commentFieldHint;
|
||||||
|
final String commentsTitle;
|
||||||
|
final String allowCommentsYes;
|
||||||
|
final String allowCommentsNo;
|
||||||
|
final String allowCommentsTitle;
|
||||||
|
final String allowCommentsDescription;
|
||||||
|
final String uploadimageTitle;
|
||||||
|
final String uploadImageDescription;
|
||||||
|
final String postTitle;
|
||||||
|
final String postTitleHint;
|
||||||
|
final String contentTitle;
|
||||||
|
final String contentDescription;
|
||||||
|
final String contentTitleHint;
|
||||||
|
final String titleErrorText;
|
||||||
|
final String contentErrorText;
|
||||||
|
final String addPost;
|
||||||
|
final String overviewButton;
|
||||||
|
final String chooseCategory;
|
||||||
|
final String addCategory;
|
||||||
|
final String postButtonTitle;
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
mixin TimelineUserStoryRoutes {
|
|
||||||
static const String timelineHome = '/timeline';
|
|
||||||
static const String timelineView = '/timeline-view/:post';
|
|
||||||
static String timelineViewPath(String postId) => '/timeline-view/$postId';
|
|
||||||
static String timelinepostCreation(String category) =>
|
|
||||||
'/timeline-post-creation/$category';
|
|
||||||
|
|
||||||
static const String timelinePostCreation =
|
|
||||||
'/timeline-post-creation/:category';
|
|
||||||
static String timelinePostOverview = '/timeline-post-overview';
|
|
||||||
static String timelineCategorySelection = '/timeline-category-selection';
|
|
||||||
}
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
import "dart:typed_data";
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class TimelineAddPostInformationScreen extends StatefulWidget {
|
||||||
|
const TimelineAddPostInformationScreen({
|
||||||
|
required this.timelineService,
|
||||||
|
required this.onTapOverview,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final void Function() onTapOverview;
|
||||||
|
final TimelineOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimelineAddPostInformationScreen> createState() =>
|
||||||
|
_TimelineAddPostInformationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineAddPostInformationScreenState
|
||||||
|
extends State<TimelineAddPostInformationScreen> {
|
||||||
|
final titleController = TextEditingController();
|
||||||
|
final contentController = TextEditingController();
|
||||||
|
bool allowedToComment = true;
|
||||||
|
Uint8List? image;
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var category =
|
||||||
|
widget.timelineService.categoryRepository.getSelectedCategory();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
category?.title.toLowerCase() ?? widget.options.translations.addPost,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
slivers: [
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 20, horizontal: 32),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.options.translations.postTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
PostInfoTextfield(
|
||||||
|
expands: false,
|
||||||
|
maxLines: 1,
|
||||||
|
controller: titleController,
|
||||||
|
hintText: widget.options.translations.postTitleHint,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return widget.options.translations.titleErrorText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.contentTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.contentDescription,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
PostInfoTextfield(
|
||||||
|
expands: false,
|
||||||
|
maxLines: 1,
|
||||||
|
controller: contentController,
|
||||||
|
hintText: widget.options.translations.contentTitleHint,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return widget.options.translations.contentErrorText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.uploadimageTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.uploadImageDescription,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
ImagePickerWidget(
|
||||||
|
onImageChanged: (pickedImage) {
|
||||||
|
image = pickedImage;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.allowCommentsTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.allowCommentsDescription,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Radio<bool>(
|
||||||
|
materialTapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: const VisualDensity(horizontal: -4),
|
||||||
|
value: true,
|
||||||
|
groupValue: allowedToComment,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
allowedToComment = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Text(widget.options.translations.allowCommentsYes),
|
||||||
|
Radio<bool>(
|
||||||
|
materialTapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
value: false,
|
||||||
|
groupValue: allowedToComment,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
allowedToComment = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(widget.options.translations.allowCommentsNo),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
fillOverscroll: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
widget.options.buttonBuilder(
|
||||||
|
title: widget.options.translations.overviewButton,
|
||||||
|
onPressed: () async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var user = await widget.timelineService.getCurrentUser();
|
||||||
|
widget.timelineService.setCurrentPost(
|
||||||
|
TimelinePost(
|
||||||
|
id: "",
|
||||||
|
creatorId: user.userId,
|
||||||
|
title: titleController.text,
|
||||||
|
content: contentController.text,
|
||||||
|
image: image,
|
||||||
|
likes: 0,
|
||||||
|
reaction: 0,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
reactionEnabled: allowedToComment,
|
||||||
|
category: category?.key,
|
||||||
|
reactions: [],
|
||||||
|
likedBy: [],
|
||||||
|
creator: user,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
widget.onTapOverview();
|
||||||
|
},
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class TimelineChooseCategoryScreen extends StatelessWidget {
|
||||||
|
const TimelineChooseCategoryScreen({
|
||||||
|
required this.timelineService,
|
||||||
|
required this.ontapCategory,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final Function(TimelineCategory category) ontapCategory;
|
||||||
|
final TimelineOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var newCategoryController = TextEditingController();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
options.translations.addPost,
|
||||||
|
style: theme.textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
options.translations.chooseCategory,
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
StreamBuilder(
|
||||||
|
stream: timelineService.getCategories(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
var categories = snapshot.data;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
...categories!
|
||||||
|
.where((category) => category.key != null)
|
||||||
|
.map(
|
||||||
|
(category) => CategoryOption(
|
||||||
|
category: category.title,
|
||||||
|
onTap: () {
|
||||||
|
timelineService.selectCategory(category.key);
|
||||||
|
|
||||||
|
ontapCategory.call(category);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (options.allowCreatingCategories)
|
||||||
|
CategoryOption(
|
||||||
|
addCategory: true,
|
||||||
|
category: options.translations.addCategory,
|
||||||
|
onTap: () async {
|
||||||
|
/// shop dialog to add category
|
||||||
|
|
||||||
|
await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
options.translations.addCategory,
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
PostInfoTextfield(
|
||||||
|
controller: newCategoryController,
|
||||||
|
hintText: "Category...",
|
||||||
|
validator: (p0) {
|
||||||
|
if (p0 == null || p0.isEmpty) {
|
||||||
|
return "Category cannot be empty";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: options.buttonBuilder(
|
||||||
|
title: options.translations.addCategory,
|
||||||
|
context: context,
|
||||||
|
onPressed: () async {
|
||||||
|
await timelineService.createCategory(
|
||||||
|
TimelineCategory(
|
||||||
|
key: newCategoryController.text
|
||||||
|
.toLowerCase(),
|
||||||
|
title: newCategoryController.text,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (context.mounted)
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Text(
|
||||||
|
"Cancel",
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryOption extends StatelessWidget {
|
||||||
|
const CategoryOption({
|
||||||
|
required this.category,
|
||||||
|
required this.onTap,
|
||||||
|
this.addCategory = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final String category;
|
||||||
|
final bool addCategory;
|
||||||
|
final Function() onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
width: 2,
|
||||||
|
color: addCategory
|
||||||
|
? Colors.black.withOpacity(0.3)
|
||||||
|
: theme.primaryColor,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (addCategory) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: addCategory
|
||||||
|
? Colors.black.withOpacity(0.3)
|
||||||
|
: theme.primaryColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
horizontal: addCategory ? 8 : 16,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
category,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: addCategory
|
||||||
|
? Colors.black.withOpacity(0.3)
|
||||||
|
: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_svg/svg.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class TimelinePostDetailScreen extends StatefulWidget {
|
||||||
|
const TimelinePostDetailScreen({
|
||||||
|
required this.post,
|
||||||
|
required this.timelineService,
|
||||||
|
required this.options,
|
||||||
|
required this.currentUserId,
|
||||||
|
required this.currentUser,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelinePost post;
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final TimelineOptions options;
|
||||||
|
final String currentUserId;
|
||||||
|
final TimelineUser? currentUser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimelinePostDetailScreen> createState() =>
|
||||||
|
_TimelinePostDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePostDetailScreenState extends State<TimelinePostDetailScreen> {
|
||||||
|
final TextEditingController _commentController = TextEditingController();
|
||||||
|
TimelineCategory? selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
selectedCategory = widget.timelineService.categoryRepository
|
||||||
|
.selectCategory(widget.post.category);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
selectedCategory?.key == null
|
||||||
|
? widget.timelineService
|
||||||
|
.getCategory(widget.post.category)
|
||||||
|
?.title ??
|
||||||
|
""
|
||||||
|
: selectedCategory?.title ?? "",
|
||||||
|
style: theme.textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: TimelinePostWidget(
|
||||||
|
post: widget.post,
|
||||||
|
timelineService: widget.timelineService,
|
||||||
|
options: widget.options,
|
||||||
|
isInDetialView: true,
|
||||||
|
currentUserId: widget.currentUserId,
|
||||||
|
onTapPost: (post) {},
|
||||||
|
onTapComments: (post) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.post.reactionEnabled)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ReactionTextfield(
|
||||||
|
controller: _commentController,
|
||||||
|
options: widget.options,
|
||||||
|
user: widget.currentUser,
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
var comment = _commentController.text;
|
||||||
|
if (comment.isNotEmpty) {
|
||||||
|
var reaction = TimelinePostReaction(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
postId: widget.post.id,
|
||||||
|
creatorId: widget.currentUserId,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
reaction: comment,
|
||||||
|
likedBy: [],
|
||||||
|
);
|
||||||
|
await widget.timelineService.postRepository
|
||||||
|
.createReaction(
|
||||||
|
widget.post,
|
||||||
|
reaction,
|
||||||
|
);
|
||||||
|
_commentController.clear();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
"assets/send.svg",
|
||||||
|
package: "flutter_timeline",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class TimelinePostOverview extends StatefulWidget {
|
||||||
|
const TimelinePostOverview({
|
||||||
|
required this.timelineService,
|
||||||
|
required this.options,
|
||||||
|
required this.onTapCreatePost,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final TimelineOptions options;
|
||||||
|
final Function(TimelinePost post) onTapCreatePost;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimelinePostOverview> createState() => _TimelinePostOverviewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePostOverviewState extends State<TimelinePostOverview> {
|
||||||
|
bool isLoading = false;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var currentPost = widget.timelineService.getCurrentPost();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
widget.options.translations.addPost,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
slivers: [
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
TimelinePostWidget(
|
||||||
|
timelineService: widget.timelineService,
|
||||||
|
post: currentPost,
|
||||||
|
options: widget.options,
|
||||||
|
currentUserId: currentPost.creatorId,
|
||||||
|
onTapPost: (post) {},
|
||||||
|
onTapComments: (post) {},
|
||||||
|
isInDetialView: true,
|
||||||
|
isInPostOverview: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
fillOverscroll: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
widget.options.buttonBuilder(
|
||||||
|
title: widget.options.translations.postButtonTitle,
|
||||||
|
onPressed: () async {
|
||||||
|
if (isLoading) return;
|
||||||
|
isLoading = true;
|
||||||
|
await widget.timelineService.createPost(currentPost);
|
||||||
|
widget.options.onCreatePost?.call(currentPost);
|
||||||
|
widget.onTapCreatePost(currentPost);
|
||||||
|
},
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
112
packages/flutter_timeline/lib/src/screens/timeline_screen.dart
Normal file
112
packages/flutter_timeline/lib/src/screens/timeline_screen.dart
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/src/models/timeline_options.dart";
|
||||||
|
import "package:flutter_timeline/src/widgets/category_list.dart";
|
||||||
|
import "package:flutter_timeline/src/widgets/post_list.dart";
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class TimelineScreen extends StatefulWidget {
|
||||||
|
const TimelineScreen({
|
||||||
|
required this.options,
|
||||||
|
required this.timelineService,
|
||||||
|
required this.onTapPost,
|
||||||
|
required this.currentUserId,
|
||||||
|
required this.onTapComments,
|
||||||
|
required this.onTapCreatePost,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final TimelineOptions options;
|
||||||
|
final Function(TimelinePost post) onTapPost;
|
||||||
|
final String currentUserId;
|
||||||
|
final Function(TimelinePost post) onTapComments;
|
||||||
|
final Function() onTapCreatePost;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimelineScreen> createState() => _TimelineScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
bool _isOnTop = true;
|
||||||
|
List<TimelineCategory> categories = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_scrollController.addListener(_updateIsOnTop);
|
||||||
|
if (widget.timelineService.getSelectedCategory() == null) {
|
||||||
|
widget.timelineService.selectCategory(widget.options.initialCategoryId);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateIsOnTop() {
|
||||||
|
setState(() {
|
||||||
|
_isOnTop = _scrollController.position.pixels < 0.1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var translations = widget.options.translations;
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
drawer: widget.options.timelineScreenDrawer,
|
||||||
|
floatingActionButton: widget.options
|
||||||
|
.floatingActionButtonBuilder(widget.onTapCreatePost, context),
|
||||||
|
appBar: widget.options.timelineScreenAppBarBuilder
|
||||||
|
?.call(context, translations.timelineTitle) ??
|
||||||
|
AppBar(
|
||||||
|
title: Text(
|
||||||
|
translations.timelineTitle,
|
||||||
|
style: theme.textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
StreamBuilder(
|
||||||
|
stream: widget.timelineService.getCategories(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
categories = snapshot.data!;
|
||||||
|
|
||||||
|
return CategoryList(
|
||||||
|
selectedCategory:
|
||||||
|
widget.timelineService.getSelectedCategory(),
|
||||||
|
categories: categories,
|
||||||
|
isOnTop: _isOnTop,
|
||||||
|
onTap: (category) {
|
||||||
|
widget.timelineService.selectCategory(category.key);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StreamBuilder(
|
||||||
|
stream: widget.timelineService.postRepository
|
||||||
|
.getPosts(widget.timelineService.getSelectedCategory()?.key),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
var posts = snapshot.data!;
|
||||||
|
return PostList(
|
||||||
|
timelineService: widget.timelineService,
|
||||||
|
currentUserId: widget.currentUserId,
|
||||||
|
controller: _scrollController,
|
||||||
|
onTapPost: widget.onTapPost,
|
||||||
|
onTapComments: widget.onTapComments,
|
||||||
|
options: widget.options,
|
||||||
|
posts: posts,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
57
packages/flutter_timeline/lib/src/widgets/category_list.dart
Normal file
57
packages/flutter_timeline/lib/src/widgets/category_list.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/src/widgets/category_widget.dart";
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class CategoryList extends StatefulWidget {
|
||||||
|
const CategoryList({
|
||||||
|
required this.categories,
|
||||||
|
required this.onTap,
|
||||||
|
required this.isOnTop,
|
||||||
|
required this.selectedCategory,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<TimelineCategory> categories;
|
||||||
|
final Function(TimelineCategory) onTap;
|
||||||
|
final bool isOnTop;
|
||||||
|
final TimelineCategory? selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CategoryList> createState() => _CategoryListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CategoryListState extends State<CategoryList> {
|
||||||
|
TimelineCategory? selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
selectedCategory = widget.selectedCategory ?? widget.categories.firstOrNull;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < widget.categories.length; i++)
|
||||||
|
CategoryWidget(
|
||||||
|
category: widget.categories[i],
|
||||||
|
onTap: (category) {
|
||||||
|
widget.onTap(category);
|
||||||
|
setState(() {
|
||||||
|
selectedCategory = category;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isOnTop: widget.isOnTop,
|
||||||
|
selected: selectedCategory?.key == widget.categories[i].key,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
134
packages/flutter_timeline/lib/src/widgets/category_widget.dart
Normal file
134
packages/flutter_timeline/lib/src/widgets/category_widget.dart
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class CategoryWidget extends StatelessWidget {
|
||||||
|
const CategoryWidget({
|
||||||
|
required this.category,
|
||||||
|
required this.onTap,
|
||||||
|
required this.isOnTop,
|
||||||
|
required this.selected,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelineCategory category;
|
||||||
|
final Function(TimelineCategory) onTap;
|
||||||
|
final bool isOnTop;
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => onTap(category),
|
||||||
|
child: AnimatedCrossFade(
|
||||||
|
crossFadeState:
|
||||||
|
isOnTop ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
firstChild: ExpandedCategoryWidget(
|
||||||
|
selected: selected,
|
||||||
|
theme: theme,
|
||||||
|
category: category,
|
||||||
|
),
|
||||||
|
secondChild: CollapsedCategoryWidget(
|
||||||
|
selected: selected,
|
||||||
|
theme: theme,
|
||||||
|
category: category,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollapsedCategoryWidget extends StatelessWidget {
|
||||||
|
const CollapsedCategoryWidget({
|
||||||
|
required this.selected,
|
||||||
|
required this.theme,
|
||||||
|
required this.category,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool selected;
|
||||||
|
final ThemeData theme;
|
||||||
|
final TimelineCategory category;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected ? theme.primaryColor : theme.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
width: 140,
|
||||||
|
height: 40,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: Text(
|
||||||
|
maxLines: 1,
|
||||||
|
category.title,
|
||||||
|
style: selected
|
||||||
|
? theme.textTheme.titleMedium
|
||||||
|
: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: selected
|
||||||
|
? theme.colorScheme.onPrimary
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpandedCategoryWidget extends StatelessWidget {
|
||||||
|
const ExpandedCategoryWidget({
|
||||||
|
required this.selected,
|
||||||
|
required this.theme,
|
||||||
|
required this.category,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool selected;
|
||||||
|
final ThemeData theme;
|
||||||
|
final TimelineCategory category;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected ? theme.primaryColor : theme.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
category.title,
|
||||||
|
style: selected
|
||||||
|
? theme.textTheme.titleMedium
|
||||||
|
: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: selected
|
||||||
|
? theme.colorScheme.onPrimary
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
117
packages/flutter_timeline/lib/src/widgets/comment_section.dart
Normal file
117
packages/flutter_timeline/lib/src/widgets/comment_section.dart
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class CommentSection extends StatefulWidget {
|
||||||
|
const CommentSection({
|
||||||
|
required this.options,
|
||||||
|
required this.post,
|
||||||
|
required this.currentUserId,
|
||||||
|
required this.timelineService,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final TimelineOptions options;
|
||||||
|
final TimelinePost post;
|
||||||
|
final String currentUserId;
|
||||||
|
final TimelineService timelineService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CommentSection> createState() => _CommentSectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentSectionState extends State<CommentSection> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.commentsTitle,
|
||||||
|
style: theme.textTheme.titleSmall!.copyWith(color: Colors.black),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
for (TimelinePostReaction reaction in widget.post.reactions ?? []) ...[
|
||||||
|
Builder(
|
||||||
|
builder: (context) => const SizedBox(),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
widget.options.userAvatarBuilder.call(reaction.creator, 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
widget.options.userNameBuilder.call(
|
||||||
|
reaction.creator,
|
||||||
|
widget.options.translations.anonymousUser,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (reaction.imageUrl != null) ...[
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: reaction.imageUrl!,
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
reaction.reaction ?? "",
|
||||||
|
style: theme.textTheme.bodySmall!.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.clip,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
var reactionIsLikedByCurrentUser =
|
||||||
|
reaction.likedBy?.contains(widget.currentUserId) ?? false;
|
||||||
|
return IconButton(
|
||||||
|
iconSize: 14,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: Icon(
|
||||||
|
reactionIsLikedByCurrentUser
|
||||||
|
? widget.options.likedIcon
|
||||||
|
: widget.options.likeIcon,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (reactionIsLikedByCurrentUser) {
|
||||||
|
await widget.timelineService.unlikePostReaction(
|
||||||
|
widget.post,
|
||||||
|
reaction,
|
||||||
|
widget.currentUserId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await widget.timelineService.likePostReaction(
|
||||||
|
widget.post,
|
||||||
|
reaction,
|
||||||
|
widget.currentUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
103
packages/flutter_timeline/lib/src/widgets/image_picker.dart
Normal file
103
packages/flutter_timeline/lib/src/widgets/image_picker.dart
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import "dart:typed_data";
|
||||||
|
|
||||||
|
import "package:dotted_border/dotted_border.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_image_picker/flutter_image_picker.dart";
|
||||||
|
|
||||||
|
class ImagePickerWidget extends StatefulWidget {
|
||||||
|
const ImagePickerWidget({required this.onImageChanged, super.key});
|
||||||
|
|
||||||
|
final Function(Uint8List?) onImageChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImagePickerWidget> createState() => _ImagePickerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImagePickerWidgetState extends State<ImagePickerWidget> {
|
||||||
|
Uint8List? image;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
image = await pickImage(context);
|
||||||
|
widget.onImageChanged(image);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: DottedBorder(
|
||||||
|
borderType: BorderType.RRect,
|
||||||
|
dashPattern: const [6, 6],
|
||||||
|
color: Colors.grey,
|
||||||
|
strokeWidth: 3,
|
||||||
|
child: image == null
|
||||||
|
? const SizedBox(
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Icon(Icons.image, size: 64),
|
||||||
|
)
|
||||||
|
: Image.memory(image!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (image != null) ...[
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
widget.onImageChanged(null);
|
||||||
|
setState(() {
|
||||||
|
image = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> pickImage(BuildContext context) async {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var result = await showModalBottomSheet<Uint8List?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
child: ImagePicker(
|
||||||
|
config: const ImagePickerConfig(),
|
||||||
|
theme: 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
class PostCreationTextfield extends StatelessWidget {
|
class PostInfoTextfield extends StatelessWidget {
|
||||||
const PostCreationTextfield({
|
const PostInfoTextfield({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
required this.validator,
|
required this.validator,
|
||||||
|
@ -30,6 +30,7 @@ class PostCreationTextfield extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
key: fieldKey,
|
key: fieldKey,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
51
packages/flutter_timeline/lib/src/widgets/post_list.dart
Normal file
51
packages/flutter_timeline/lib/src/widgets/post_list.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class PostList extends StatelessWidget {
|
||||||
|
const PostList({
|
||||||
|
required this.controller,
|
||||||
|
required this.posts,
|
||||||
|
required this.timelineService,
|
||||||
|
required this.options,
|
||||||
|
required this.onTapPost,
|
||||||
|
required this.currentUserId,
|
||||||
|
required this.onTapComments,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScrollController controller;
|
||||||
|
final List<TimelinePost> posts;
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final TimelineOptions options;
|
||||||
|
final Function(TimelinePost post) onTapPost;
|
||||||
|
final String currentUserId;
|
||||||
|
final Function(TimelinePost post) onTapComments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
itemCount: posts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
posts.sort(
|
||||||
|
(b, a) => a.createdAt.compareTo(b.createdAt),
|
||||||
|
);
|
||||||
|
var post = posts[index];
|
||||||
|
// var post = posts[index];
|
||||||
|
return options.postBuilder?.call(
|
||||||
|
context: context,
|
||||||
|
onTap: onTapPost,
|
||||||
|
post: post,
|
||||||
|
) ??
|
||||||
|
TimelinePostWidget(
|
||||||
|
timelineService: timelineService,
|
||||||
|
currentUserId: currentUserId,
|
||||||
|
onTapPost: onTapPost,
|
||||||
|
options: options,
|
||||||
|
post: post,
|
||||||
|
onTapComments: onTapComments,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class MoreOptionsButton extends StatelessWidget {
|
||||||
|
const MoreOptionsButton({
|
||||||
|
required this.timelineService,
|
||||||
|
required this.post,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final TimelinePost post;
|
||||||
|
final TimelineOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => PopupMenuButton(
|
||||||
|
onSelected: (value) async {
|
||||||
|
if (value == "delete") {
|
||||||
|
options.onPostDelete ?? await timelineService.deletePost(post.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||||
|
PopupMenuItem(
|
||||||
|
value: "delete",
|
||||||
|
child: Text(options.translations.deletePostTitle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Icon(
|
||||||
|
Icons.more_horiz_rounded,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class ReactionTextfield extends StatelessWidget {
|
||||||
|
const ReactionTextfield({
|
||||||
|
required this.options,
|
||||||
|
required this.controller,
|
||||||
|
required this.suffixIcon,
|
||||||
|
required this.user,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelineUser? user;
|
||||||
|
final TimelineOptions options;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final Widget suffixIcon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return ColoredBox(
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
bottom: 20,
|
||||||
|
right: 16,
|
||||||
|
top: 20,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: options.userAvatarBuilder(
|
||||||
|
user,
|
||||||
|
26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: 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: options.translations.commentFieldHint,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
import "dart:async";
|
||||||
|
|
||||||
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_timeline_interface/flutter_timeline_interface.dart';
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
class TappableImage extends StatefulWidget {
|
class TappableImage extends StatefulWidget {
|
||||||
const TappableImage({
|
const TappableImage({
|
||||||
|
@ -15,7 +15,7 @@ class TappableImage extends StatefulWidget {
|
||||||
|
|
||||||
final TimelinePost post;
|
final TimelinePost post;
|
||||||
final String userId;
|
final String userId;
|
||||||
final Future<bool> Function({required bool liked}) onLike;
|
final Future<bool> Function() onLike;
|
||||||
final (Icon?, Icon?) likeAndDislikeIcon;
|
final (Icon?, Icon?) likeAndDislikeIcon;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -73,12 +73,7 @@ class _TappableImageState extends State<TappableImage>
|
||||||
loading = true;
|
loading = true;
|
||||||
await animationController.forward();
|
await animationController.forward();
|
||||||
|
|
||||||
var liked = await widget.onLike(
|
var liked = await widget.onLike();
|
||||||
liked: widget.post.likedBy?.contains(
|
|
||||||
widget.userId,
|
|
||||||
) ??
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
|
@ -101,15 +96,19 @@ class _TappableImageState extends State<TappableImage>
|
||||||
scale: 1 + animation.value * 0.1,
|
scale: 1 + animation.value * 0.1,
|
||||||
child: widget.post.imageUrl != null
|
child: widget.post.imageUrl != null
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: widget.post.imageUrl ?? '',
|
height: 250,
|
||||||
|
imageUrl: widget.post.imageUrl ?? "",
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
fit: BoxFit.fitHeight,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: Image.memory(
|
: widget.post.image != null
|
||||||
width: double.infinity,
|
? Image.memory(
|
||||||
widget.post.image!,
|
width: double.infinity,
|
||||||
fit: BoxFit.fitHeight,
|
widget.post.image!,
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
|
height: 250,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
250
packages/flutter_timeline/lib/src/widgets/timeline_post.dart
Normal file
250
packages/flutter_timeline/lib/src/widgets/timeline_post.dart
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_timeline/flutter_timeline.dart";
|
||||||
|
|
||||||
|
class TimelinePostWidget extends StatefulWidget {
|
||||||
|
const TimelinePostWidget({
|
||||||
|
required this.post,
|
||||||
|
required this.timelineService,
|
||||||
|
required this.options,
|
||||||
|
required this.currentUserId,
|
||||||
|
required this.onTapPost,
|
||||||
|
required this.onTapComments,
|
||||||
|
this.isInDetialView = false,
|
||||||
|
this.isInPostOverview = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelinePost post;
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final TimelineOptions options;
|
||||||
|
final String currentUserId;
|
||||||
|
final Function(TimelinePost post) onTapPost;
|
||||||
|
final bool isInDetialView;
|
||||||
|
final Function(TimelinePost post) onTapComments;
|
||||||
|
final bool isInPostOverview;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimelinePostWidget> createState() => _TimelinePostWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var translations = widget.options.translations;
|
||||||
|
var user = widget.post.creator;
|
||||||
|
var options = widget.options;
|
||||||
|
var post = widget.post;
|
||||||
|
var isLikedByCurrentUser =
|
||||||
|
widget.post.likedBy?.contains(widget.currentUserId) ?? false;
|
||||||
|
var likesTitle = widget.post.likes == 1
|
||||||
|
? translations.oneLikeTitle
|
||||||
|
: translations.multipleLikesTitle;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
top: 20,
|
||||||
|
bottom: widget.isInDetialView ? 100 : 0,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
options.userAvatarBuilder.call(user, 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
options.userNameBuilder
|
||||||
|
.call(user, options.translations.anonymousUser, context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (post.creatorId == widget.currentUserId &&
|
||||||
|
!widget.isInPostOverview &&
|
||||||
|
!widget.isInDetialView)
|
||||||
|
MoreOptionsButton(
|
||||||
|
timelineService: widget.timelineService,
|
||||||
|
options: options,
|
||||||
|
post: post,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
if (post.imageUrl != null || post.image != null) ...[
|
||||||
|
if (options.doubleTapToLike) ...[
|
||||||
|
TappableImage(
|
||||||
|
post: post,
|
||||||
|
onLike: () async {
|
||||||
|
if (isLikedByCurrentUser) {
|
||||||
|
widget.options.onTapUnlike ??
|
||||||
|
widget.timelineService.unlikePost(
|
||||||
|
widget.post.id,
|
||||||
|
widget.currentUserId,
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
widget.options.onTapLike ??
|
||||||
|
widget.timelineService.likePost(
|
||||||
|
widget.post.id,
|
||||||
|
widget.currentUserId,
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
userId: widget.currentUserId,
|
||||||
|
likeAndDislikeIcon: (
|
||||||
|
Icon(options.likeIcon),
|
||||||
|
Icon(options.likedIcon)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
if (post.imageUrl != null)
|
||||||
|
Container(
|
||||||
|
height: 250,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: CachedNetworkImageProvider(widget.post.imageUrl!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (post.image != null)
|
||||||
|
Container(
|
||||||
|
height: 250,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: MemoryImage(widget.post.image!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
var postIsLikedByCurrentUser =
|
||||||
|
post.likedBy?.contains(widget.currentUserId) ?? false;
|
||||||
|
return IconButton(
|
||||||
|
iconSize: options.iconSize,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: Icon(
|
||||||
|
postIsLikedByCurrentUser
|
||||||
|
? widget.options.likedIcon
|
||||||
|
: widget.options.likeIcon,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (postIsLikedByCurrentUser) {
|
||||||
|
await widget.timelineService.unlikePost(
|
||||||
|
post.id,
|
||||||
|
widget.currentUserId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await widget.timelineService.likePost(
|
||||||
|
widget.post.id,
|
||||||
|
widget.currentUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (post.reactionEnabled) ...[
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: Icon(
|
||||||
|
options.commentIcon,
|
||||||
|
size: options.iconSize,
|
||||||
|
color: options.iconColor,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
widget.onTapComments(widget.post);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
if (!widget.isInPostOverview) ...[
|
||||||
|
Text(
|
||||||
|
"${widget.post.likes} $likesTitle",
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
options.userNameBuilder.call(
|
||||||
|
user,
|
||||||
|
options.translations.anonymousUser,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
widget.post.title,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (widget.isInDetialView) ...[
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.post.content,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.dateFormat(context).format(widget.post.createdAt),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.post.reactionEnabled)
|
||||||
|
if (!widget.isInPostOverview)
|
||||||
|
CommentSection(
|
||||||
|
options: options,
|
||||||
|
post: post,
|
||||||
|
currentUserId: widget.currentUserId,
|
||||||
|
timelineService: widget.timelineService,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!widget.isInDetialView)
|
||||||
|
InkWell(
|
||||||
|
onTap: () => widget.onTapPost(widget.post),
|
||||||
|
child: Text(
|
||||||
|
translations.viewPostTitle,
|
||||||
|
style: theme.textTheme.titleSmall
|
||||||
|
?.copyWith(color: Colors.black.withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +1,37 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
name: flutter_timeline
|
name: flutter_timeline
|
||||||
description: Visual elements and interface combined into one package
|
description: "A new Flutter package project."
|
||||||
version: 5.1.0
|
version: 6.0.0
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.1.3 <4.0.0"
|
sdk: ^3.5.1
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_timeline_view:
|
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
version: ^5.1.0
|
|
||||||
|
|
||||||
flutter_timeline_interface:
|
flutter_image_picker:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||||
version: ^5.1.0
|
version: ^4.0.0
|
||||||
collection: any
|
timeline_repository_interface:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_timeline
|
||||||
|
path: packages/timeline_repository_interface
|
||||||
|
ref: 6.0.0
|
||||||
|
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
intl: 0.19.0
|
||||||
|
flutter_svg: ^2.0.10+1
|
||||||
|
dotted_border: ^2.1.0
|
||||||
|
collection: ^1.18.0
|
||||||
|
|
||||||
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: 7.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
assets:
|
||||||
|
- assets/
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1,11 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
///
|
|
||||||
library flutter_timeline_firebase;
|
|
||||||
|
|
||||||
export 'src/config/firebase_timeline_options.dart';
|
|
||||||
export 'src/service/firebase_post_service.dart';
|
|
||||||
export 'src/service/firebase_timeline_service.dart';
|
|
||||||
export 'src/service/firebase_user_service.dart';
|
|
|
@ -1,18 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class FirebaseTimelineOptions {
|
|
||||||
const FirebaseTimelineOptions({
|
|
||||||
this.usersCollectionName = 'users',
|
|
||||||
this.timelineCollectionName = 'timeline',
|
|
||||||
this.timelineCategoryCollectionName = 'timeline_categories',
|
|
||||||
});
|
|
||||||
|
|
||||||
final String usersCollectionName;
|
|
||||||
final String timelineCollectionName;
|
|
||||||
final String timelineCategoryCollectionName;
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class FirebaseUserDocument {
|
|
||||||
const FirebaseUserDocument({
|
|
||||||
this.firstName,
|
|
||||||
this.lastName,
|
|
||||||
this.imageUrl,
|
|
||||||
this.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
FirebaseUserDocument.fromJson(
|
|
||||||
Map<String, Object?> json,
|
|
||||||
String userId,
|
|
||||||
) : this(
|
|
||||||
userId: userId,
|
|
||||||
firstName: json['first_name'] as String?,
|
|
||||||
lastName: json['last_name'] as String?,
|
|
||||||
imageUrl: json['image_url'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
final String? firstName;
|
|
||||||
final String? lastName;
|
|
||||||
final String? imageUrl;
|
|
||||||
final String? userId;
|
|
||||||
|
|
||||||
Map<String, Object?> toJson() => {
|
|
||||||
'first_name': firstName,
|
|
||||||
'last_name': lastName,
|
|
||||||
'image_url': imageUrl,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,495 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class FirebaseTimelinePostService
|
|
||||||
with TimelineUserService, ChangeNotifier
|
|
||||||
implements TimelinePostService {
|
|
||||||
FirebaseTimelinePostService({
|
|
||||||
required TimelineUserService userService,
|
|
||||||
FirebaseApp? app,
|
|
||||||
FirebaseTimelineOptions? options,
|
|
||||||
}) {
|
|
||||||
var appInstance = app ?? Firebase.app();
|
|
||||||
_db = FirebaseFirestore.instanceFor(app: appInstance);
|
|
||||||
_storage = FirebaseStorage.instanceFor(app: appInstance);
|
|
||||||
_userService = userService;
|
|
||||||
_options = options ?? const FirebaseTimelineOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
late FirebaseFirestore _db;
|
|
||||||
late FirebaseStorage _storage;
|
|
||||||
late TimelineUserService _userService;
|
|
||||||
late FirebaseTimelineOptions _options;
|
|
||||||
|
|
||||||
final Map<String, TimelinePosterUserModel> _users = {};
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelinePost> posts = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelineCategory> categories = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimelineCategory? selectedCategory;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
|
||||||
var postId = const Uuid().v4();
|
|
||||||
var user = await _userService.getUser(post.creatorId);
|
|
||||||
var updatedPost = post.copyWith(id: postId, creator: user);
|
|
||||||
if (post.image != null) {
|
|
||||||
var imageRef =
|
|
||||||
_storage.ref().child('${_options.timelineCollectionName}/$postId');
|
|
||||||
var result = await imageRef.putData(post.image!);
|
|
||||||
var imageUrl = await result.ref.getDownloadURL();
|
|
||||||
updatedPost = updatedPost.copyWith(imageUrl: imageUrl);
|
|
||||||
}
|
|
||||||
var postRef =
|
|
||||||
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
|
||||||
await postRef.set(updatedPost.toJson());
|
|
||||||
posts.add(updatedPost);
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> deletePost(TimelinePost post) async {
|
|
||||||
posts = posts.where((element) => element.id != post.id).toList();
|
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
|
||||||
await postRef.delete();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> deletePostReaction(
|
|
||||||
TimelinePost post,
|
|
||||||
String reactionId,
|
|
||||||
) async {
|
|
||||||
if (post.reactions != null && post.reactions!.isNotEmpty) {
|
|
||||||
var reaction =
|
|
||||||
post.reactions!.firstWhere((element) => element.id == reactionId);
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
reaction: post.reaction - 1,
|
|
||||||
reactions: (post.reactions ?? [])..remove(reaction),
|
|
||||||
);
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
var postRef =
|
|
||||||
_db.collection(_options.timelineCollectionName).doc(post.id);
|
|
||||||
await postRef.update({
|
|
||||||
'reaction': FieldValue.increment(-1),
|
|
||||||
'reactions': FieldValue.arrayRemove(
|
|
||||||
[reaction.toJsonWithMicroseconds()],
|
|
||||||
),
|
|
||||||
});
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> fetchPostDetails(TimelinePost post) async {
|
|
||||||
var reactions = post.reactions ?? [];
|
|
||||||
var updatedReactions = <TimelinePostReaction>[];
|
|
||||||
for (var reaction in reactions) {
|
|
||||||
var user = await _userService.getUser(reaction.creatorId);
|
|
||||||
if (user != null) {
|
|
||||||
updatedReactions.add(reaction.copyWith(creator: user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
reactions: updatedReactions,
|
|
||||||
creator: await _userService.getUser(post.creatorId),
|
|
||||||
);
|
|
||||||
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
|
||||||
var snapshot = (category != null)
|
|
||||||
? await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.where('category', isEqualTo: category)
|
|
||||||
.get()
|
|
||||||
: await _db.collection(_options.timelineCollectionName).get();
|
|
||||||
|
|
||||||
var fetchedPosts = <TimelinePost>[];
|
|
||||||
for (var doc in snapshot.docs) {
|
|
||||||
var data = doc.data();
|
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
|
||||||
fetchedPosts.add(post);
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = fetchedPosts;
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
return posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<TimelinePost>> fetchPostsPaginated(
|
|
||||||
String? category,
|
|
||||||
int limit,
|
|
||||||
) async {
|
|
||||||
// only take posts that are in our category
|
|
||||||
var oldestPost = posts
|
|
||||||
.where(
|
|
||||||
(element) => category == null || element.category == category,
|
|
||||||
)
|
|
||||||
.fold(
|
|
||||||
posts.first,
|
|
||||||
(previousValue, element) =>
|
|
||||||
(previousValue.createdAt.isBefore(element.createdAt))
|
|
||||||
? previousValue
|
|
||||||
: element,
|
|
||||||
);
|
|
||||||
var snapshot = (category != null)
|
|
||||||
? await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.where('category', isEqualTo: category)
|
|
||||||
.orderBy('created_at', descending: true)
|
|
||||||
.startAfter([oldestPost])
|
|
||||||
.limit(limit)
|
|
||||||
.get()
|
|
||||||
: await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.orderBy('created_at', descending: true)
|
|
||||||
.startAfter([oldestPost.createdAt])
|
|
||||||
.limit(limit)
|
|
||||||
.get();
|
|
||||||
// add the new posts to the list
|
|
||||||
var newPosts = <TimelinePost>[];
|
|
||||||
for (var doc in snapshot.docs) {
|
|
||||||
var data = doc.data();
|
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
|
||||||
newPosts.add(post);
|
|
||||||
}
|
|
||||||
posts = [...posts, ...newPosts];
|
|
||||||
notifyListeners();
|
|
||||||
return newPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> fetchPost(TimelinePost post) async {
|
|
||||||
var doc = await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.doc(post.id)
|
|
||||||
.get();
|
|
||||||
var data = doc.data();
|
|
||||||
if (data == null) return post;
|
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
|
||||||
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
|
|
||||||
creator: user,
|
|
||||||
);
|
|
||||||
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
|
||||||
// fetch all posts between now and the newest posts we have
|
|
||||||
var newestPostWeHave = posts
|
|
||||||
.where(
|
|
||||||
(element) => category == null || element.category == category,
|
|
||||||
)
|
|
||||||
.fold(
|
|
||||||
posts.first,
|
|
||||||
(previousValue, element) =>
|
|
||||||
(previousValue.createdAt.isAfter(element.createdAt))
|
|
||||||
? previousValue
|
|
||||||
: element,
|
|
||||||
);
|
|
||||||
var snapshot = (category != null)
|
|
||||||
? await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.where('category', isEqualTo: category)
|
|
||||||
.orderBy('created_at', descending: true)
|
|
||||||
.endBefore([newestPostWeHave.createdAt]).get()
|
|
||||||
: await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.orderBy('created_at', descending: true)
|
|
||||||
.endBefore([newestPostWeHave.createdAt]).get();
|
|
||||||
// add the new posts to the list
|
|
||||||
var newPosts = <TimelinePost>[];
|
|
||||||
for (var doc in snapshot.docs) {
|
|
||||||
var data = doc.data();
|
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
|
||||||
newPosts.add(post);
|
|
||||||
}
|
|
||||||
posts = [...posts, ...newPosts];
|
|
||||||
notifyListeners();
|
|
||||||
return newPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost?> getPost(String postId) async {
|
|
||||||
var post = await _db
|
|
||||||
.collection(_options.timelineCollectionName)
|
|
||||||
.doc(postId)
|
|
||||||
.withConverter<TimelinePost>(
|
|
||||||
fromFirestore: (snapshot, _) => TimelinePost.fromJson(
|
|
||||||
snapshot.id,
|
|
||||||
snapshot.data()!,
|
|
||||||
),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
)
|
|
||||||
.get();
|
|
||||||
return post.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelinePost> getPosts(String? category) => posts
|
|
||||||
.where((element) => category == null || element.category == category)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> likePost(String userId, TimelinePost post) async {
|
|
||||||
// update the post with the new like
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
likes: post.likes + 1,
|
|
||||||
likedBy: [...post.likedBy ?? [], userId],
|
|
||||||
);
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
|
||||||
await postRef.update({
|
|
||||||
'likes': FieldValue.increment(1),
|
|
||||||
'liked_by': FieldValue.arrayUnion([userId]),
|
|
||||||
});
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> unlikePost(String userId, TimelinePost post) async {
|
|
||||||
// update the post with the new like
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
likes: post.likes - 1,
|
|
||||||
likedBy: post.likedBy?..remove(userId),
|
|
||||||
);
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
|
||||||
await postRef.update({
|
|
||||||
'likes': FieldValue.increment(-1),
|
|
||||||
'liked_by': FieldValue.arrayRemove([userId]),
|
|
||||||
});
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> reactToPost(
|
|
||||||
TimelinePost post,
|
|
||||||
TimelinePostReaction reaction, {
|
|
||||||
Uint8List? image,
|
|
||||||
}) async {
|
|
||||||
var reactionId = const Uuid().v4();
|
|
||||||
// also fetch the user information and add it to the reaction
|
|
||||||
var user = await _userService.getUser(reaction.creatorId);
|
|
||||||
var updatedReaction = reaction.copyWith(id: reactionId, creator: user);
|
|
||||||
if (image != null) {
|
|
||||||
var imageRef = _storage
|
|
||||||
.ref()
|
|
||||||
.child('${_options.timelineCollectionName}/${post.id}/$reactionId}');
|
|
||||||
var result = await imageRef.putData(image);
|
|
||||||
var imageUrl = await result.ref.getDownloadURL();
|
|
||||||
updatedReaction = updatedReaction.copyWith(imageUrl: imageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
reaction: post.reaction + 1,
|
|
||||||
reactions: post.reactions?..add(updatedReaction),
|
|
||||||
);
|
|
||||||
|
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
|
||||||
await postRef.update({
|
|
||||||
'reaction': FieldValue.increment(1),
|
|
||||||
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
|
||||||
});
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
CollectionReference<FirebaseUserDocument> get _userCollection => _db
|
|
||||||
.collection(_options.usersCollectionName)
|
|
||||||
.withConverter<FirebaseUserDocument>(
|
|
||||||
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
|
|
||||||
snapshot.data()!,
|
|
||||||
snapshot.id,
|
|
||||||
),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
Future<TimelinePosterUserModel?> getUser(String userId) async {
|
|
||||||
if (_users.containsKey(userId)) {
|
|
||||||
return _users[userId]!;
|
|
||||||
}
|
|
||||||
var data = (await _userCollection.doc(userId).get()).data();
|
|
||||||
|
|
||||||
var user = data == null
|
|
||||||
? TimelinePosterUserModel(userId: userId)
|
|
||||||
: TimelinePosterUserModel(
|
|
||||||
userId: userId,
|
|
||||||
firstName: data.firstName,
|
|
||||||
lastName: data.lastName,
|
|
||||||
imageUrl: data.imageUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
_users[userId] = 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
|
||||||
import 'package:flutter_timeline_firebase/flutter_timeline_firebase.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
|
|
||||||
class FirebaseTimelineService implements TimelineService {
|
|
||||||
FirebaseTimelineService({
|
|
||||||
this.options,
|
|
||||||
this.app,
|
|
||||||
this.firebasePostService,
|
|
||||||
this.firebaseUserService,
|
|
||||||
}) {
|
|
||||||
firebaseUserService ??= FirebaseTimelineUserService(
|
|
||||||
options: options,
|
|
||||||
app: app,
|
|
||||||
);
|
|
||||||
|
|
||||||
firebasePostService ??= FirebaseTimelinePostService(
|
|
||||||
userService: userService,
|
|
||||||
options: options,
|
|
||||||
app: app,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final FirebaseTimelineOptions? options;
|
|
||||||
final FirebaseApp? app;
|
|
||||||
TimelinePostService? firebasePostService;
|
|
||||||
TimelineUserService? firebaseUserService;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimelinePostService get postService {
|
|
||||||
if (firebasePostService != null) {
|
|
||||||
return firebasePostService!;
|
|
||||||
} else {
|
|
||||||
return FirebaseTimelinePostService(
|
|
||||||
userService: userService,
|
|
||||||
options: options,
|
|
||||||
app: app,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimelineUserService get userService {
|
|
||||||
if (firebaseUserService != null) {
|
|
||||||
return firebaseUserService!;
|
|
||||||
} else {
|
|
||||||
return FirebaseTimelineUserService(
|
|
||||||
options: options,
|
|
||||||
app: app,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
|
||||||
import 'package:flutter_timeline_firebase/src/config/firebase_timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
|
|
||||||
class FirebaseTimelineUserService implements TimelineUserService {
|
|
||||||
FirebaseTimelineUserService({
|
|
||||||
FirebaseApp? app,
|
|
||||||
FirebaseTimelineOptions? options,
|
|
||||||
}) {
|
|
||||||
var appInstance = app ?? Firebase.app();
|
|
||||||
_db = FirebaseFirestore.instanceFor(app: appInstance);
|
|
||||||
_options = options ?? const FirebaseTimelineOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
late FirebaseFirestore _db;
|
|
||||||
late FirebaseTimelineOptions _options;
|
|
||||||
|
|
||||||
final Map<String, TimelinePosterUserModel> _users = {};
|
|
||||||
|
|
||||||
CollectionReference<FirebaseUserDocument> get _userCollection => _db
|
|
||||||
.collection(_options.usersCollectionName)
|
|
||||||
.withConverter<FirebaseUserDocument>(
|
|
||||||
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
|
|
||||||
snapshot.data()!,
|
|
||||||
snapshot.id,
|
|
||||||
),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
Future<TimelinePosterUserModel?> getUser(String userId) async {
|
|
||||||
if (_users.containsKey(userId)) {
|
|
||||||
return _users[userId]!;
|
|
||||||
}
|
|
||||||
var data = (await _userCollection.doc(userId).get()).data();
|
|
||||||
|
|
||||||
var user = data == null
|
|
||||||
? TimelinePosterUserModel(userId: userId)
|
|
||||||
: TimelinePosterUserModel(
|
|
||||||
userId: userId,
|
|
||||||
firstName: data.firstName,
|
|
||||||
lastName: data.lastName,
|
|
||||||
imageUrl: data.imageUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
_users[userId] = user;
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: flutter_timeline_firebase
|
|
||||||
description: Implementation of the Flutter Timeline interface for Firebase.
|
|
||||||
version: 5.1.0
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ">=3.1.3 <4.0.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
cloud_firestore: ^4.13.1
|
|
||||||
firebase_core: ^2.22.0
|
|
||||||
firebase_storage: ^11.5.1
|
|
||||||
uuid: ^4.2.1
|
|
||||||
collection: ^1.18.0
|
|
||||||
flutter_timeline_interface:
|
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
version: ^5.1.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_lints: ^2.0.0
|
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 6.0.0
|
|
||||||
|
|
||||||
flutter:
|
|
|
@ -1,13 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1,14 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
///
|
|
||||||
library flutter_timeline_interface;
|
|
||||||
|
|
||||||
export 'src/model/timeline_category.dart';
|
|
||||||
export 'src/model/timeline_post.dart';
|
|
||||||
export 'src/model/timeline_poster.dart';
|
|
||||||
export 'src/model/timeline_reaction.dart';
|
|
||||||
export 'src/services/filter_service.dart';
|
|
||||||
export 'src/services/timeline_post_service.dart';
|
|
||||||
export 'src/services/timeline_service.dart';
|
|
||||||
export 'src/services/user_service.dart';
|
|
|
@ -1,51 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class TimelinePosterUserModel {
|
|
||||||
const TimelinePosterUserModel({
|
|
||||||
required this.userId,
|
|
||||||
this.firstName,
|
|
||||||
this.lastName,
|
|
||||||
this.imageUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory TimelinePosterUserModel.fromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
String userId,
|
|
||||||
) =>
|
|
||||||
TimelinePosterUserModel(
|
|
||||||
userId: userId,
|
|
||||||
firstName: json['first_name'] as String?,
|
|
||||||
lastName: json['last_name'] as String?,
|
|
||||||
imageUrl: json['image_url'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
final String userId;
|
|
||||||
final String? firstName;
|
|
||||||
final String? lastName;
|
|
||||||
final String? imageUrl;
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'first_name': firstName,
|
|
||||||
'last_name': lastName,
|
|
||||||
'image_url': imageUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
String? get fullName {
|
|
||||||
var fullName = '';
|
|
||||||
|
|
||||||
if (firstName != null && lastName != null) {
|
|
||||||
fullName += '$firstName $lastName';
|
|
||||||
} else if (firstName != null) {
|
|
||||||
fullName += firstName!;
|
|
||||||
} else if (lastName != null) {
|
|
||||||
fullName += lastName!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullName == '' ? null : fullName;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
|
|
||||||
mixin TimelineFilterService on TimelinePostService {
|
|
||||||
List<TimelinePost> filterPosts(
|
|
||||||
String filterWord,
|
|
||||||
Map<String, dynamic> options,
|
|
||||||
) {
|
|
||||||
var filteredPosts = posts
|
|
||||||
.where(
|
|
||||||
(post) => post.title.toLowerCase().contains(
|
|
||||||
filterWord.toLowerCase(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return filteredPosts;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
|
|
||||||
abstract class TimelinePostService with ChangeNotifier {
|
|
||||||
List<TimelinePost> posts = [];
|
|
||||||
List<TimelineCategory> categories = [];
|
|
||||||
TimelineCategory? selectedCategory;
|
|
||||||
|
|
||||||
Future<void> deletePost(TimelinePost post);
|
|
||||||
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
|
||||||
Future<TimelinePost> createPost(TimelinePost post);
|
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category);
|
|
||||||
Future<TimelinePost> fetchPost(TimelinePost post);
|
|
||||||
Future<List<TimelinePost>> fetchPostsPaginated(String? category, int limit);
|
|
||||||
Future<TimelinePost?> getPost(String postId);
|
|
||||||
List<TimelinePost> getPosts(String? category);
|
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category);
|
|
||||||
Future<TimelinePost> fetchPostDetails(TimelinePost post);
|
|
||||||
Future<TimelinePost> reactToPost(
|
|
||||||
TimelinePost post,
|
|
||||||
TimelinePostReaction reaction, {
|
|
||||||
Uint8List image,
|
|
||||||
});
|
|
||||||
Future<TimelinePost> likePost(String userId, TimelinePost post);
|
|
||||||
Future<TimelinePost> unlikePost(String userId, TimelinePost post);
|
|
||||||
|
|
||||||
Future<List<TimelineCategory>> fetchCategories();
|
|
||||||
Future<bool> addCategory(TimelineCategory category);
|
|
||||||
Future<TimelinePost> likeReaction(
|
|
||||||
String userId,
|
|
||||||
TimelinePost post,
|
|
||||||
String reactionId,
|
|
||||||
);
|
|
||||||
Future<TimelinePost> unlikeReaction(
|
|
||||||
String userId,
|
|
||||||
TimelinePost post,
|
|
||||||
String reactionId,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import 'package:flutter_timeline_interface/src/services/timeline_post_service.dart';
|
|
||||||
import 'package:flutter_timeline_interface/src/services/user_service.dart';
|
|
||||||
|
|
||||||
class TimelineService {
|
|
||||||
TimelineService({
|
|
||||||
required this.postService,
|
|
||||||
this.userService,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TimelinePostService postService;
|
|
||||||
final TimelineUserService? userService;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
|
|
||||||
|
|
||||||
mixin TimelineUserService {
|
|
||||||
Future<TimelinePosterUserModel?> getUser(String userId);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: flutter_timeline_interface
|
|
||||||
description: Interface for the service of the Flutter Timeline component
|
|
||||||
version: 5.1.0
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: '>=3.1.3 <4.0.0'
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_lints: ^2.0.0
|
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 6.0.0
|
|
||||||
|
|
||||||
flutter:
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../../CHANGELOG.md
|
|
|
@ -1 +0,0 @@
|
||||||
../../LICENSE
|
|
|
@ -1 +0,0 @@
|
||||||
../../README.md
|
|
|
@ -1,13 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1,20 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
///
|
|
||||||
library flutter_timeline_view;
|
|
||||||
|
|
||||||
export 'src/config/timeline_options.dart';
|
|
||||||
export 'src/config/timeline_paddings.dart';
|
|
||||||
export 'src/config/timeline_styles.dart';
|
|
||||||
export 'src/config/timeline_theme.dart';
|
|
||||||
export 'src/config/timeline_translations.dart';
|
|
||||||
export 'src/screens/timeline_post_creation_screen.dart';
|
|
||||||
export 'src/screens/timeline_post_overview_screen.dart';
|
|
||||||
export 'src/screens/timeline_post_screen.dart';
|
|
||||||
export 'src/screens/timeline_screen.dart';
|
|
||||||
export 'src/screens/timeline_selection_screen.dart';
|
|
||||||
export 'src/services/local_post_service.dart';
|
|
||||||
export 'src/widgets/category_selector.dart';
|
|
||||||
export 'src/widgets/category_selector_button.dart';
|
|
||||||
export 'src/widgets/timeline_post_widget.dart';
|
|
|
@ -1,244 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_paddings.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_theme.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class TimelineOptions {
|
|
||||||
const TimelineOptions({
|
|
||||||
this.theme = const TimelineTheme(),
|
|
||||||
this.translations = const TimelineTranslations.empty(),
|
|
||||||
this.paddings = const TimelinePaddingOptions(),
|
|
||||||
this.imagePickerConfig = const ImagePickerConfig(),
|
|
||||||
this.imagePickerTheme,
|
|
||||||
this.timelinePostHeight,
|
|
||||||
this.sortCommentsAscending = true,
|
|
||||||
this.sortPostsAscending = false,
|
|
||||||
this.doubleTapTolike = false,
|
|
||||||
this.iconsWithValues = false,
|
|
||||||
this.likeAndDislikeIconsForDoubleTap = const (
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_rounded,
|
|
||||||
color: Color(0xFFC3007A),
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
this.itemInfoBuilder,
|
|
||||||
this.dateFormat,
|
|
||||||
this.timeFormat,
|
|
||||||
this.buttonBuilder,
|
|
||||||
this.textInputBuilder,
|
|
||||||
this.dividerBuilder,
|
|
||||||
this.userAvatarBuilder,
|
|
||||||
this.anonymousAvatarBuilder,
|
|
||||||
this.nameBuilder,
|
|
||||||
this.iconSize = 24,
|
|
||||||
this.postWidgetHeight,
|
|
||||||
this.filterOptions = const FilterOptions(),
|
|
||||||
this.categoriesOptions = const CategoriesOptions(),
|
|
||||||
this.requireImageForPost = false,
|
|
||||||
this.minTitleLength,
|
|
||||||
this.maxTitleLength,
|
|
||||||
this.minContentLength,
|
|
||||||
this.maxContentLength,
|
|
||||||
this.categorySelectorButtonBuilder,
|
|
||||||
this.postOverviewButtonBuilder,
|
|
||||||
this.deletionDialogBuilder,
|
|
||||||
this.listHeaderBuilder,
|
|
||||||
this.titleInputDecoration,
|
|
||||||
this.contentInputDecoration,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Theming options for the timeline
|
|
||||||
final TimelineTheme theme;
|
|
||||||
|
|
||||||
/// The format to display the post date in
|
|
||||||
final DateFormat? dateFormat;
|
|
||||||
|
|
||||||
/// The format to display the post time in
|
|
||||||
final DateFormat? timeFormat;
|
|
||||||
|
|
||||||
/// Whether to sort comments ascending or descending
|
|
||||||
final bool sortCommentsAscending;
|
|
||||||
|
|
||||||
/// Whether to sort posts ascending or descending
|
|
||||||
final bool? sortPostsAscending;
|
|
||||||
|
|
||||||
/// The height of a post in the timeline
|
|
||||||
final double? timelinePostHeight;
|
|
||||||
|
|
||||||
/// Class that contains all the translations used in the timeline
|
|
||||||
final TimelineTranslations translations;
|
|
||||||
|
|
||||||
/// Class that contains all the paddings used in the timeline
|
|
||||||
final TimelinePaddingOptions paddings;
|
|
||||||
|
|
||||||
final ButtonBuilder? buttonBuilder;
|
|
||||||
|
|
||||||
final TextInputBuilder? textInputBuilder;
|
|
||||||
|
|
||||||
final UserAvatarBuilder? userAvatarBuilder;
|
|
||||||
|
|
||||||
/// When the imageUrl is null this anonymousAvatarBuilder will be used
|
|
||||||
/// You can use it to display a default avatarW
|
|
||||||
final UserAvatarBuilder? anonymousAvatarBuilder;
|
|
||||||
|
|
||||||
final String Function(TimelinePosterUserModel?)? nameBuilder;
|
|
||||||
|
|
||||||
/// ImagePickerTheme can be used to change the UI of the
|
|
||||||
/// Image Picker Widget to change the text/icons to your liking.
|
|
||||||
final ImagePickerTheme? imagePickerTheme;
|
|
||||||
|
|
||||||
/// ImagePickerConfig can be used to define the
|
|
||||||
/// size and quality for the uploaded image.
|
|
||||||
final ImagePickerConfig imagePickerConfig;
|
|
||||||
|
|
||||||
/// Whether to allow double tap to like
|
|
||||||
final bool doubleTapTolike;
|
|
||||||
|
|
||||||
/// The icons to display when double tap to like is enabled
|
|
||||||
final (Icon?, Icon?) likeAndDislikeIconsForDoubleTap;
|
|
||||||
|
|
||||||
/// Whether to display the icons with values
|
|
||||||
final bool iconsWithValues;
|
|
||||||
|
|
||||||
/// The builder for the item info, all below the like and comment buttons
|
|
||||||
final Widget Function({required TimelinePost post})? itemInfoBuilder;
|
|
||||||
|
|
||||||
/// The builder for the divider
|
|
||||||
final Widget Function()? dividerBuilder;
|
|
||||||
|
|
||||||
/// Size of icons like the comment and like icons. Dafualts to 26
|
|
||||||
final double iconSize;
|
|
||||||
|
|
||||||
/// Sets a predefined height for the postWidget.
|
|
||||||
final double? postWidgetHeight;
|
|
||||||
|
|
||||||
/// Options for filtering
|
|
||||||
final FilterOptions filterOptions;
|
|
||||||
|
|
||||||
/// Options for using the category selector.
|
|
||||||
final CategoriesOptions categoriesOptions;
|
|
||||||
|
|
||||||
/// Require image for post
|
|
||||||
final bool requireImageForPost;
|
|
||||||
|
|
||||||
/// Minimum length of the title
|
|
||||||
final int? minTitleLength;
|
|
||||||
|
|
||||||
/// Maximum length of the title
|
|
||||||
final int? maxTitleLength;
|
|
||||||
|
|
||||||
/// Minimum length of the post content
|
|
||||||
final int? minContentLength;
|
|
||||||
|
|
||||||
/// Maximum length of the post content
|
|
||||||
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 {
|
|
||||||
const CategoriesOptions({
|
|
||||||
this.categoryButtonBuilder,
|
|
||||||
this.categorySelectorHorizontalPadding,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// List of categories that the user can select.
|
|
||||||
/// If this is null no categories will be shown.
|
|
||||||
|
|
||||||
/// Abilty to override the standard category selector
|
|
||||||
final Widget Function(
|
|
||||||
TimelineCategory category,
|
|
||||||
Function() onTap,
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
bool selected,
|
|
||||||
bool isOnTop,
|
|
||||||
)? categoryButtonBuilder;
|
|
||||||
|
|
||||||
/// Overides the standard horizontal padding of the whole category selector.
|
|
||||||
final double? categorySelectorHorizontalPadding;
|
|
||||||
|
|
||||||
TimelineCategory? getCategoryByKey(
|
|
||||||
List<TimelineCategory> categories,
|
|
||||||
BuildContext context,
|
|
||||||
String? key,
|
|
||||||
) =>
|
|
||||||
categories.firstWhereOrNull((category) => category.key == key);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FilterOptions {
|
|
||||||
const FilterOptions({
|
|
||||||
this.initialFilterWord,
|
|
||||||
this.searchBarBuilder,
|
|
||||||
this.onFilterEnabledChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Set a value to search through posts. When set the searchbar is shown.
|
|
||||||
/// If null no searchbar is shown.
|
|
||||||
final String? initialFilterWord;
|
|
||||||
|
|
||||||
// Possibilty to override the standard search bar.
|
|
||||||
final Widget Function(
|
|
||||||
Future<List<TimelinePost>> Function(
|
|
||||||
String filterWord,
|
|
||||||
) search,
|
|
||||||
)? searchBarBuilder;
|
|
||||||
|
|
||||||
final void Function({required bool filterEnabled})? onFilterEnabledChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef ButtonBuilder = Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
VoidCallback onPressed,
|
|
||||||
String text, {
|
|
||||||
bool enabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
typedef TextInputBuilder = Widget Function(
|
|
||||||
TextEditingController controller,
|
|
||||||
Widget? suffixIcon,
|
|
||||||
String hintText,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef UserAvatarBuilder = Widget? Function(
|
|
||||||
TimelinePosterUserModel user,
|
|
||||||
double size,
|
|
||||||
);
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class TimelineTextStyles {
|
|
||||||
/// Options to update all the texts in the timeline view
|
|
||||||
/// with different textstyles
|
|
||||||
const TimelineTextStyles({
|
|
||||||
this.viewPostStyle,
|
|
||||||
this.listPostTitleStyle,
|
|
||||||
this.listPostCreatorTitleStyle,
|
|
||||||
this.listCreatorNameStyle,
|
|
||||||
this.listPostLikeTitleAndAmount,
|
|
||||||
this.deletePostStyle,
|
|
||||||
this.categorySelectionDescriptionStyle,
|
|
||||||
this.categorySelectionTitleStyle,
|
|
||||||
this.noPostsStyle,
|
|
||||||
this.errorTextStyle,
|
|
||||||
this.postCreatorTitleStyle,
|
|
||||||
this.postCreatorNameStyle,
|
|
||||||
this.postTitleStyle,
|
|
||||||
this.postLikeTitleAndAmount,
|
|
||||||
this.postCreatedAtStyle,
|
|
||||||
this.categoryTitleStyle,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The TextStyle for the text indicating that you can view a post
|
|
||||||
final TextStyle? viewPostStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the creatorname at the top of the card
|
|
||||||
/// when it is in the list
|
|
||||||
final TextStyle? listPostCreatorTitleStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the post title when it is in the list
|
|
||||||
final TextStyle? listPostTitleStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the creatorname at the bottom of the card
|
|
||||||
/// when it is in the list
|
|
||||||
final TextStyle? listCreatorNameStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the amount of like and name of the likes at
|
|
||||||
/// the bottom of the card when it is in the list
|
|
||||||
final TextStyle? listPostLikeTitleAndAmount;
|
|
||||||
|
|
||||||
/// The TextStyle for the deletion text that shows in the popupmenu
|
|
||||||
final TextStyle? deletePostStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the category explainer on the selection page
|
|
||||||
final TextStyle? categorySelectionDescriptionStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the category items in the list on the selection page
|
|
||||||
final TextStyle? categorySelectionTitleStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the text when there are no posts
|
|
||||||
final TextStyle? noPostsStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for all error texts
|
|
||||||
final TextStyle? errorTextStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the creatorname at the top of the post page
|
|
||||||
final TextStyle? postCreatorTitleStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the creatorname at the bottom of the post page
|
|
||||||
final TextStyle? postCreatorNameStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the title of the post on the post page
|
|
||||||
final TextStyle? postTitleStyle;
|
|
||||||
|
|
||||||
/// The TextStyle for the amount of likes and name of the likes
|
|
||||||
/// on the post page
|
|
||||||
final TextStyle? postLikeTitleAndAmount;
|
|
||||||
|
|
||||||
/// The TextStyle for the creation time of the post
|
|
||||||
final TextStyle? postCreatedAtStyle;
|
|
||||||
|
|
||||||
final TextStyle? categoryTitleStyle;
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_styles.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class TimelineTheme {
|
|
||||||
const TimelineTheme({
|
|
||||||
this.iconColor,
|
|
||||||
this.likeIcon,
|
|
||||||
this.commentIcon,
|
|
||||||
this.likedIcon,
|
|
||||||
this.sendIcon,
|
|
||||||
this.moreIcon,
|
|
||||||
this.deleteIcon,
|
|
||||||
this.categorySelectionButtonBorderColor,
|
|
||||||
this.categorySelectionButtonBackgroundColor,
|
|
||||||
this.categorySelectionButtonSelectedTextColor,
|
|
||||||
this.categorySelectionButtonUnselectedTextColor,
|
|
||||||
this.postCreationFloatingActionButtonColor,
|
|
||||||
this.textStyles = const TimelineTextStyles(),
|
|
||||||
});
|
|
||||||
|
|
||||||
final Color? iconColor;
|
|
||||||
|
|
||||||
/// The icon to display when the post is not yet liked
|
|
||||||
final Widget? likeIcon;
|
|
||||||
|
|
||||||
/// The icon to display to indicate that a post has comments enabled
|
|
||||||
final Widget? commentIcon;
|
|
||||||
|
|
||||||
/// The icon to display when the post is liked
|
|
||||||
final Widget? likedIcon;
|
|
||||||
|
|
||||||
/// The icon to display to submit a comment
|
|
||||||
final Widget? sendIcon;
|
|
||||||
|
|
||||||
/// The icon for more actions (open delete menu)
|
|
||||||
final Widget? moreIcon;
|
|
||||||
|
|
||||||
/// The icon for delete action (delete post)
|
|
||||||
final Widget? deleteIcon;
|
|
||||||
|
|
||||||
/// The text style overrides for all the texts in the timeline
|
|
||||||
final TimelineTextStyles textStyles;
|
|
||||||
|
|
||||||
/// The color of the border around the category in the selection screen
|
|
||||||
final Color? categorySelectionButtonBorderColor;
|
|
||||||
|
|
||||||
/// The color of the background of the category selection button in the
|
|
||||||
/// selection screen
|
|
||||||
final Color? categorySelectionButtonBackgroundColor;
|
|
||||||
|
|
||||||
/// The color of the 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;
|
|
||||||
}
|
|
|
@ -1,263 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
|
|
||||||
/// Class that holds all the translations for the timeline component view and
|
|
||||||
/// the corresponding userstory
|
|
||||||
class TimelineTranslations {
|
|
||||||
/// TimelineTranslations constructor where everything is required use this
|
|
||||||
/// if you want to be sure to have all translations specified
|
|
||||||
/// If you just want the default values use the empty constructor
|
|
||||||
/// and optionally override the values with the copyWith method
|
|
||||||
const TimelineTranslations({
|
|
||||||
required this.anonymousUser,
|
|
||||||
required this.noPosts,
|
|
||||||
required this.noPostsWithFilter,
|
|
||||||
required this.title,
|
|
||||||
required this.titleHintText,
|
|
||||||
required this.content,
|
|
||||||
required this.contentHintText,
|
|
||||||
required this.contentDescription,
|
|
||||||
required this.uploadImage,
|
|
||||||
required this.uploadImageDescription,
|
|
||||||
required this.allowComments,
|
|
||||||
required this.allowCommentsDescription,
|
|
||||||
required this.commentsTitleOnPost,
|
|
||||||
required this.checkPost,
|
|
||||||
required this.deletePost,
|
|
||||||
required this.deleteReaction,
|
|
||||||
required this.deleteConfirmationMessage,
|
|
||||||
required this.deleteConfirmationTitle,
|
|
||||||
required this.deleteCancelButton,
|
|
||||||
required this.deleteButton,
|
|
||||||
required this.viewPost,
|
|
||||||
required this.oneLikeTitle,
|
|
||||||
required this.multipleLikesTitle,
|
|
||||||
required this.commentsTitle,
|
|
||||||
required this.firstComment,
|
|
||||||
required this.writeComment,
|
|
||||||
required this.postLoadingError,
|
|
||||||
required this.timelineSelectionDescription,
|
|
||||||
required this.searchHint,
|
|
||||||
required this.postOverview,
|
|
||||||
required this.postIn,
|
|
||||||
required this.postCreation,
|
|
||||||
required this.yes,
|
|
||||||
required this.no,
|
|
||||||
required this.timeLineScreenTitle,
|
|
||||||
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({
|
|
||||||
this.anonymousUser = 'Anonymous user',
|
|
||||||
this.noPosts = 'No posts yet',
|
|
||||||
this.noPostsWithFilter = 'No posts with this filter',
|
|
||||||
this.title = 'Title',
|
|
||||||
this.titleHintText = 'Title...',
|
|
||||||
this.content = 'Content',
|
|
||||||
this.contentHintText = 'Content...',
|
|
||||||
this.contentDescription = 'What do you want to share?',
|
|
||||||
this.uploadImage = 'Upload image',
|
|
||||||
this.uploadImageDescription = 'Upload an image to your message (optional)',
|
|
||||||
this.allowComments = 'Are people allowed to comment?',
|
|
||||||
this.allowCommentsDescription =
|
|
||||||
'Indicate whether people are allowed to respond',
|
|
||||||
this.commentsTitleOnPost = 'Comments',
|
|
||||||
this.checkPost = 'Overview',
|
|
||||||
this.deletePost = 'Delete post',
|
|
||||||
this.deleteConfirmationTitle = 'Delete Post',
|
|
||||||
this.deleteConfirmationMessage =
|
|
||||||
'Are you sure you want to delete this post?',
|
|
||||||
this.deleteButton = 'Delete',
|
|
||||||
this.deleteCancelButton = 'Cancel',
|
|
||||||
this.deleteReaction = 'Delete Reaction',
|
|
||||||
this.viewPost = 'View post',
|
|
||||||
this.oneLikeTitle = 'like',
|
|
||||||
this.multipleLikesTitle = 'likes',
|
|
||||||
this.commentsTitle = 'Are people allowed to comment?',
|
|
||||||
this.firstComment = 'Be the first to comment',
|
|
||||||
this.writeComment = 'Write your comment here...',
|
|
||||||
this.postLoadingError = 'Something went wrong while loading the post',
|
|
||||||
this.timelineSelectionDescription = 'Choose a category',
|
|
||||||
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 noPostsWithFilter;
|
|
||||||
final String anonymousUser;
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final String content;
|
|
||||||
final String contentDescription;
|
|
||||||
final String uploadImage;
|
|
||||||
final String uploadImageDescription;
|
|
||||||
final String allowComments;
|
|
||||||
final String allowCommentsDescription;
|
|
||||||
final String checkPost;
|
|
||||||
|
|
||||||
final String titleHintText;
|
|
||||||
final String contentHintText;
|
|
||||||
final String titleErrorText;
|
|
||||||
final String contentErrorText;
|
|
||||||
|
|
||||||
final String deletePost;
|
|
||||||
final String deleteConfirmationTitle;
|
|
||||||
final String deleteConfirmationMessage;
|
|
||||||
final String deleteButton;
|
|
||||||
final String deleteCancelButton;
|
|
||||||
|
|
||||||
final String deleteReaction;
|
|
||||||
final String viewPost;
|
|
||||||
final String oneLikeTitle;
|
|
||||||
final String multipleLikesTitle;
|
|
||||||
final String commentsTitle;
|
|
||||||
final String commentsTitleOnPost;
|
|
||||||
final String writeComment;
|
|
||||||
final String firstComment;
|
|
||||||
final String postLoadingError;
|
|
||||||
|
|
||||||
final String timelineSelectionDescription;
|
|
||||||
|
|
||||||
final String searchHint;
|
|
||||||
|
|
||||||
final String postOverview;
|
|
||||||
final String postIn;
|
|
||||||
final String postCreation;
|
|
||||||
|
|
||||||
final String createCategoryPopuptitle;
|
|
||||||
final String addCategoryTitle;
|
|
||||||
final String addCategorySubmitButton;
|
|
||||||
final String addCategoryCancelButtton;
|
|
||||||
final String addCategoryHintText;
|
|
||||||
final String addCategoryErrorText;
|
|
||||||
|
|
||||||
final String yes;
|
|
||||||
final String no;
|
|
||||||
final String timeLineScreenTitle;
|
|
||||||
|
|
||||||
/// Method to override the default values of the translations
|
|
||||||
TimelineTranslations copyWith({
|
|
||||||
String? noPosts,
|
|
||||||
String? noPostsWithFilter,
|
|
||||||
String? anonymousUser,
|
|
||||||
String? title,
|
|
||||||
String? content,
|
|
||||||
String? contentDescription,
|
|
||||||
String? uploadImage,
|
|
||||||
String? uploadImageDescription,
|
|
||||||
String? allowComments,
|
|
||||||
String? allowCommentsDescription,
|
|
||||||
String? commentsTitleOnPost,
|
|
||||||
String? checkPost,
|
|
||||||
String? deletePost,
|
|
||||||
String? deleteConfirmationTitle,
|
|
||||||
String? deleteConfirmationMessage,
|
|
||||||
String? deleteButton,
|
|
||||||
String? deleteCancelButton,
|
|
||||||
String? deleteReaction,
|
|
||||||
String? viewPost,
|
|
||||||
String? oneLikeTitle,
|
|
||||||
String? multipleLikesTitle,
|
|
||||||
String? commentsTitle,
|
|
||||||
String? writeComment,
|
|
||||||
String? firstComment,
|
|
||||||
String? postLoadingError,
|
|
||||||
String? timelineSelectionDescription,
|
|
||||||
String? searchHint,
|
|
||||||
String? postOverview,
|
|
||||||
String? postIn,
|
|
||||||
String? postCreation,
|
|
||||||
String? titleHintText,
|
|
||||||
String? contentHintText,
|
|
||||||
String? yes,
|
|
||||||
String? no,
|
|
||||||
String? timeLineScreenTitle,
|
|
||||||
String? createCategoryPopuptitle,
|
|
||||||
String? addCategoryTitle,
|
|
||||||
String? addCategorySubmitButton,
|
|
||||||
String? addCategoryCancelButtton,
|
|
||||||
String? addCategoryHintText,
|
|
||||||
String? addCategoryErrorText,
|
|
||||||
String? titleErrorText,
|
|
||||||
String? contentErrorText,
|
|
||||||
}) =>
|
|
||||||
TimelineTranslations(
|
|
||||||
noPosts: noPosts ?? this.noPosts,
|
|
||||||
noPostsWithFilter: noPostsWithFilter ?? this.noPostsWithFilter,
|
|
||||||
anonymousUser: anonymousUser ?? this.anonymousUser,
|
|
||||||
title: title ?? this.title,
|
|
||||||
content: content ?? this.content,
|
|
||||||
contentDescription: contentDescription ?? this.contentDescription,
|
|
||||||
uploadImage: uploadImage ?? this.uploadImage,
|
|
||||||
uploadImageDescription:
|
|
||||||
uploadImageDescription ?? this.uploadImageDescription,
|
|
||||||
allowComments: allowComments ?? this.allowComments,
|
|
||||||
allowCommentsDescription:
|
|
||||||
allowCommentsDescription ?? this.allowCommentsDescription,
|
|
||||||
commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost,
|
|
||||||
checkPost: checkPost ?? this.checkPost,
|
|
||||||
deletePost: deletePost ?? this.deletePost,
|
|
||||||
deleteConfirmationTitle:
|
|
||||||
deleteConfirmationTitle ?? this.deleteConfirmationTitle,
|
|
||||||
deleteConfirmationMessage:
|
|
||||||
deleteConfirmationMessage ?? this.deleteConfirmationMessage,
|
|
||||||
deleteButton: deleteButton ?? this.deleteButton,
|
|
||||||
deleteCancelButton: deleteCancelButton ?? this.deleteCancelButton,
|
|
||||||
deleteReaction: deleteReaction ?? this.deleteReaction,
|
|
||||||
viewPost: viewPost ?? this.viewPost,
|
|
||||||
oneLikeTitle: oneLikeTitle ?? this.oneLikeTitle,
|
|
||||||
multipleLikesTitle: multipleLikesTitle ?? this.multipleLikesTitle,
|
|
||||||
commentsTitle: commentsTitle ?? this.commentsTitle,
|
|
||||||
writeComment: writeComment ?? this.writeComment,
|
|
||||||
firstComment: firstComment ?? this.firstComment,
|
|
||||||
postLoadingError: postLoadingError ?? this.postLoadingError,
|
|
||||||
timelineSelectionDescription:
|
|
||||||
timelineSelectionDescription ?? this.timelineSelectionDescription,
|
|
||||||
searchHint: searchHint ?? this.searchHint,
|
|
||||||
postOverview: postOverview ?? this.postOverview,
|
|
||||||
postIn: postIn ?? this.postIn,
|
|
||||||
postCreation: postCreation ?? this.postCreation,
|
|
||||||
titleHintText: titleHintText ?? this.titleHintText,
|
|
||||||
contentHintText: contentHintText ?? this.contentHintText,
|
|
||||||
yes: yes ?? this.yes,
|
|
||||||
no: no ?? this.no,
|
|
||||||
timeLineScreenTitle: timeLineScreenTitle ?? this.timeLineScreenTitle,
|
|
||||||
addCategoryTitle: addCategoryTitle ?? this.addCategoryTitle,
|
|
||||||
addCategorySubmitButton:
|
|
||||||
addCategorySubmitButton ?? this.addCategorySubmitButton,
|
|
||||||
addCategoryCancelButtton:
|
|
||||||
addCategoryCancelButtton ?? this.addCategoryCancelButtton,
|
|
||||||
addCategoryHintText: addCategoryHintText ?? this.addCategoryHintText,
|
|
||||||
createCategoryPopuptitle:
|
|
||||||
createCategoryPopuptitle ?? this.createCategoryPopuptitle,
|
|
||||||
addCategoryErrorText: addCategoryErrorText ?? this.addCategoryErrorText,
|
|
||||||
titleErrorText: titleErrorText ?? this.titleErrorText,
|
|
||||||
contentErrorText: contentErrorText ?? this.contentErrorText,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,395 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:dotted_border/dotted_border.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/post_creation_textfield.dart';
|
|
||||||
|
|
||||||
class TimelinePostCreationScreen extends StatefulWidget {
|
|
||||||
const TimelinePostCreationScreen({
|
|
||||||
required this.userId,
|
|
||||||
required this.onPostCreated,
|
|
||||||
required this.service,
|
|
||||||
required this.options,
|
|
||||||
this.postCategory,
|
|
||||||
this.onPostOverview,
|
|
||||||
this.enablePostOverviewScreen = false,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String userId;
|
|
||||||
|
|
||||||
final String? postCategory;
|
|
||||||
|
|
||||||
/// called when the post is created
|
|
||||||
final Function(TimelinePost) onPostCreated;
|
|
||||||
|
|
||||||
/// The service to use for creating the post
|
|
||||||
final TimelineService service;
|
|
||||||
|
|
||||||
/// The options for the timeline
|
|
||||||
final TimelineOptions options;
|
|
||||||
|
|
||||||
/// Nullable callback for routing to the post overview
|
|
||||||
final void Function(TimelinePost)? onPostOverview;
|
|
||||||
final bool enablePostOverviewScreen;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TimelinePostCreationScreen> createState() =>
|
|
||||||
_TimelinePostCreationScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelinePostCreationScreenState
|
|
||||||
extends State<TimelinePostCreationScreen> {
|
|
||||||
TextEditingController titleController = TextEditingController();
|
|
||||||
TextEditingController contentController = TextEditingController();
|
|
||||||
Uint8List? image;
|
|
||||||
bool allowComments = false;
|
|
||||||
bool titleIsValid = false;
|
|
||||||
bool contentIsValid = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
titleController.addListener(_listenForInputs);
|
|
||||||
contentController.addListener(_listenForInputs);
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenForInputs() {
|
|
||||||
titleIsValid = titleController.text.isNotEmpty;
|
|
||||||
contentIsValid = contentController.text.isNotEmpty;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
var formkey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var imageRequired = widget.options.requireImageForPost;
|
|
||||||
|
|
||||||
Future<void> onPostCreated() async {
|
|
||||||
var user = await widget.service.userService?.getUser(widget.userId);
|
|
||||||
var post = TimelinePost(
|
|
||||||
id: 'Post${Random().nextInt(1000)}',
|
|
||||||
creatorId: widget.userId,
|
|
||||||
title: titleController.text,
|
|
||||||
category: widget.postCategory,
|
|
||||||
content: contentController.text,
|
|
||||||
likes: 0,
|
|
||||||
likedBy: const [],
|
|
||||||
reaction: 0,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
reactionEnabled: allowComments,
|
|
||||||
image: image,
|
|
||||||
creator: user,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.enablePostOverviewScreen) {
|
|
||||||
widget.onPostOverview?.call(post);
|
|
||||||
} else {
|
|
||||||
widget.onPostCreated.call(post);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => FocusScope.of(context).unfocus(),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: widget.options.paddings.mainPadding,
|
|
||||||
child: Form(
|
|
||||||
key: formkey,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.options.translations.title,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
widget.options.textInputBuilder?.call(
|
|
||||||
titleController,
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
) ??
|
|
||||||
PostCreationTextfield(
|
|
||||||
fieldKey: const ValueKey('title'),
|
|
||||||
controller: titleController,
|
|
||||||
hintText: widget.options.translations.titleHintText,
|
|
||||||
textMaxLength: widget.options.maxTitleLength,
|
|
||||||
decoration: widget.options.titleInputDecoration,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
expands: null,
|
|
||||||
minLines: null,
|
|
||||||
maxLines: 1,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return widget.options.translations.titleErrorText;
|
|
||||||
}
|
|
||||||
if (value.trim().isEmpty) {
|
|
||||||
return widget.options.translations.titleErrorText;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.content,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.contentDescription,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
PostCreationTextfield(
|
|
||||||
fieldKey: const ValueKey('content'),
|
|
||||||
controller: contentController,
|
|
||||||
hintText: widget.options.translations.contentHintText,
|
|
||||||
textMaxLength: null,
|
|
||||||
decoration: widget.options.contentInputDecoration,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
expands: false,
|
|
||||||
minLines: null,
|
|
||||||
maxLines: null,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return widget.options.translations.contentErrorText;
|
|
||||||
}
|
|
||||||
if (value.trim().isEmpty) {
|
|
||||||
return widget.options.translations.contentErrorText;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.uploadImage,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.uploadImageDescription,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
var result = await showModalBottomSheet<Uint8List?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
color: theme.colorScheme.surface,
|
|
||||||
child: ImagePicker(
|
|
||||||
config: widget.options.imagePickerConfig,
|
|
||||||
theme: widget.options.imagePickerTheme ??
|
|
||||||
ImagePickerTheme(
|
|
||||||
titleStyle: theme.textTheme.titleMedium,
|
|
||||||
iconSize: 40,
|
|
||||||
selectImageText: 'UPLOAD FILE',
|
|
||||||
makePhotoText: 'TAKE PICTURE',
|
|
||||||
selectImageIcon: const Icon(
|
|
||||||
size: 40,
|
|
||||||
Icons.insert_drive_file,
|
|
||||||
),
|
|
||||||
closeButtonBuilder: (onTap) => TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
onTap();
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Cancel',
|
|
||||||
style: theme.textTheme.bodyMedium!
|
|
||||||
.copyWith(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (result != null) {
|
|
||||||
setState(() {
|
|
||||||
image = result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
|
|
||||||
class TimelinePostOverviewScreen extends StatelessWidget {
|
|
||||||
const TimelinePostOverviewScreen({
|
|
||||||
required this.timelinePost,
|
|
||||||
required this.options,
|
|
||||||
required this.service,
|
|
||||||
required this.onPostSubmit,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
final TimelinePost timelinePost;
|
|
||||||
final TimelineOptions options;
|
|
||||||
final TimelineService service;
|
|
||||||
final void Function(TimelinePost) onPostSubmit;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var isSubmitted = false;
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TimelinePostScreen(
|
|
||||||
userId: timelinePost.creatorId,
|
|
||||||
options: options,
|
|
||||||
post: timelinePost,
|
|
||||||
onPostDelete: () async {},
|
|
||||||
service: service,
|
|
||||||
isOverviewScreen: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
options.postOverviewButtonBuilder?.call(
|
|
||||||
context,
|
|
||||||
() {
|
|
||||||
if (isSubmitted) return;
|
|
||||||
isSubmitted = true;
|
|
||||||
onPostSubmit(timelinePost);
|
|
||||||
},
|
|
||||||
options.translations.postIn,
|
|
||||||
timelinePost,
|
|
||||||
) ??
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,695 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_svg/svg.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class TimelinePostScreen extends StatefulWidget {
|
|
||||||
const TimelinePostScreen({
|
|
||||||
required this.userId,
|
|
||||||
required this.service,
|
|
||||||
required this.options,
|
|
||||||
required this.post,
|
|
||||||
required this.onPostDelete,
|
|
||||||
this.allowAllDeletion = false,
|
|
||||||
this.isOverviewScreen = false,
|
|
||||||
this.onUserTap,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The user id of the current user
|
|
||||||
final String userId;
|
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
|
||||||
/// only the posts of the current user
|
|
||||||
final bool allowAllDeletion;
|
|
||||||
|
|
||||||
/// The timeline service to fetch the post details
|
|
||||||
final TimelineService service;
|
|
||||||
|
|
||||||
/// Options to configure the timeline screens
|
|
||||||
final TimelineOptions options;
|
|
||||||
|
|
||||||
/// The post to show
|
|
||||||
final TimelinePost post;
|
|
||||||
|
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
|
||||||
final Function(String userId)? onUserTap;
|
|
||||||
|
|
||||||
final VoidCallback onPostDelete;
|
|
||||||
|
|
||||||
final bool? isOverviewScreen;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|
||||||
TimelinePost? post;
|
|
||||||
bool isLoading = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
await loadPostDetails();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadPostDetails() async {
|
|
||||||
try {
|
|
||||||
var loadedPost =
|
|
||||||
await widget.service.postService.fetchPostDetails(widget.post);
|
|
||||||
setState(() {
|
|
||||||
post = loadedPost;
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
} on Exception catch (_) {
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updatePost(TimelinePost newPost) {
|
|
||||||
setState(() {
|
|
||||||
post = newPost;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var dateFormat = widget.options.dateFormat ??
|
|
||||||
DateFormat(
|
|
||||||
"dd/MM/yyyy 'at' HH:mm",
|
|
||||||
Localizations.localeOf(context).languageCode,
|
|
||||||
);
|
|
||||||
if (isLoading) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.post == null) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
widget.options.translations.postLoadingError,
|
|
||||||
style: widget.options.theme.textStyles.errorTextStyle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var post = this.post!;
|
|
||||||
post.reactions?.sort(
|
|
||||||
(a, b) => widget.options.sortCommentsAscending
|
|
||||||
? a.createdAt.compareTo(b.createdAt)
|
|
||||||
: b.createdAt.compareTo(a.createdAt),
|
|
||||||
);
|
|
||||||
var isLikedByUser = post.likedBy?.contains(widget.userId) ?? false;
|
|
||||||
|
|
||||||
var textInputBuilder = widget.options.textInputBuilder ??
|
|
||||||
(controller, suffixIcon, hintText) => TextField(
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: const BorderSide(
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
borderSide: const BorderSide(
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 0,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
hintText: widget.options.translations.writeComment,
|
|
||||||
hintStyle: theme.textTheme.bodyMedium!.copyWith(
|
|
||||||
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
fillColor: Colors.white,
|
|
||||||
filled: true,
|
|
||||||
border: const OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.all(
|
|
||||||
Radius.circular(25),
|
|
||||||
),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
suffixIcon: suffixIcon,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
RefreshIndicator.adaptive(
|
|
||||||
onRefresh: () async {
|
|
||||||
updatePost(
|
|
||||||
await widget.service.postService.fetchPostDetails(
|
|
||||||
await widget.service.postService.fetchPost(
|
|
||||||
post,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: widget.options.paddings.postPadding,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (post.creator != null)
|
|
||||||
InkWell(
|
|
||||||
onTap: widget.onUserTap != null
|
|
||||||
? () =>
|
|
||||||
widget.onUserTap?.call(post.creator!.userId)
|
|
||||||
: null,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (post.creator!.imageUrl != null) ...[
|
|
||||||
widget.options.userAvatarBuilder?.call(
|
|
||||||
post.creator!,
|
|
||||||
28,
|
|
||||||
) ??
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
backgroundImage:
|
|
||||||
CachedNetworkImageProvider(
|
|
||||||
post.creator!.imageUrl!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
widget.options.anonymousAvatarBuilder?.call(
|
|
||||||
post.creator!,
|
|
||||||
28,
|
|
||||||
) ??
|
|
||||||
const CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(
|
|
||||||
widget.options.nameBuilder
|
|
||||||
?.call(post.creator) ??
|
|
||||||
post.creator?.fullName ??
|
|
||||||
widget.options.translations.anonymousUser,
|
|
||||||
style: widget.options.theme.textStyles
|
|
||||||
.postCreatorTitleStyle ??
|
|
||||||
theme.textTheme.titleSmall!.copyWith(
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (!(widget.isOverviewScreen ?? false) &&
|
|
||||||
(widget.allowAllDeletion ||
|
|
||||||
post.creator?.userId == widget.userId)) ...[
|
|
||||||
PopupMenuButton(
|
|
||||||
onSelected: (value) async {
|
|
||||||
if (value == 'delete') {
|
|
||||||
await showPostDeletionConfirmationDialog(
|
|
||||||
widget.options,
|
|
||||||
context,
|
|
||||||
widget.onPostDelete,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (BuildContext context) =>
|
|
||||||
<PopupMenuEntry<String>>[
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'delete',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.options.translations.deletePost,
|
|
||||||
style: widget.options.theme.textStyles
|
|
||||||
.deletePostStyle ??
|
|
||||||
theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
widget.options.theme.deleteIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: widget.options.theme.moreIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.more_horiz_rounded,
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// image of the posts
|
|
||||||
if (post.imageUrl != null || post.image != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: widget.options.doubleTapTolike
|
|
||||||
? TappableImage(
|
|
||||||
likeAndDislikeIcon: widget
|
|
||||||
.options.likeAndDislikeIconsForDoubleTap,
|
|
||||||
post: 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,
|
|
||||||
post,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
result = await widget.service.postService
|
|
||||||
.unlikePost(
|
|
||||||
userId,
|
|
||||||
post,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadPostDetails();
|
|
||||||
|
|
||||||
return result.likedBy?.contains(userId) ??
|
|
||||||
false;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: post.image != null
|
|
||||||
? Image.memory(
|
|
||||||
width: double.infinity,
|
|
||||||
post.image!,
|
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
)
|
|
||||||
: CachedNetworkImage(
|
|
||||||
width: double.infinity,
|
|
||||||
imageUrl: post.imageUrl!,
|
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
// post information
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: () async {
|
|
||||||
if (widget.isOverviewScreen ?? false) return;
|
|
||||||
if (isLikedByUser) {
|
|
||||||
updatePost(
|
|
||||||
await widget.service.postService.unlikePost(
|
|
||||||
widget.userId,
|
|
||||||
post,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
} else {
|
|
||||||
updatePost(
|
|
||||||
await widget.service.postService.likePost(
|
|
||||||
widget.userId,
|
|
||||||
post,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: isLikedByUser
|
|
||||||
? widget.options.theme.likedIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_rounded,
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
size: widget.options.iconSize,
|
|
||||||
)
|
|
||||||
: widget.options.theme.likeIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_outline_outlined,
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
size: widget.options.iconSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
if (post.reactionEnabled)
|
|
||||||
widget.options.theme.commentIcon ??
|
|
||||||
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),
|
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
|
||||||
if (widget.isOverviewScreen != null
|
|
||||||
? !widget.isOverviewScreen!
|
|
||||||
: false) ...[
|
|
||||||
Text(
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
'${post.likes} ${post.likes > 1 ? widget.options.translations.multipleLikesTitle : widget.options.translations.oneLikeTitle}',
|
|
||||||
style: widget.options.theme.textStyles
|
|
||||||
.postLikeTitleAndAmount ??
|
|
||||||
theme.textTheme.titleSmall
|
|
||||||
?.copyWith(color: Colors.black),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
text: widget.options.nameBuilder?.call(post.creator) ??
|
|
||||||
post.creator?.fullName ??
|
|
||||||
widget.options.translations.anonymousUser,
|
|
||||||
style: widget
|
|
||||||
.options.theme.textStyles.postCreatorNameStyle ??
|
|
||||||
theme.textTheme.titleSmall!
|
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: post.title,
|
|
||||||
style:
|
|
||||||
widget.options.theme.textStyles.postTitleStyle ??
|
|
||||||
theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
post.content,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${dateFormat.format(post.createdAt)} ',
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
|
||||||
if (post.reactionEnabled && widget.isOverviewScreen != null
|
|
||||||
? !widget.isOverviewScreen!
|
|
||||||
: false) ...[
|
|
||||||
Text(
|
|
||||||
widget.options.translations.commentsTitleOnPost,
|
|
||||||
style: theme.textTheme.titleSmall!
|
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
),
|
|
||||||
for (var reaction
|
|
||||||
in post.reactions ?? <TimelinePostReaction>[]) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
GestureDetector(
|
|
||||||
onLongPressStart: (details) async {
|
|
||||||
if (reaction.creatorId == widget.userId ||
|
|
||||||
widget.allowAllDeletion) {
|
|
||||||
var overlay = Overlay.of(context)
|
|
||||||
.context
|
|
||||||
.findRenderObject()! as RenderBox;
|
|
||||||
var position = RelativeRect.fromRect(
|
|
||||||
Rect.fromPoints(
|
|
||||||
details.globalPosition,
|
|
||||||
details.globalPosition,
|
|
||||||
),
|
|
||||||
Offset.zero & overlay.size,
|
|
||||||
);
|
|
||||||
// Show popup menu for deletion
|
|
||||||
var value = await showMenu<String>(
|
|
||||||
context: context,
|
|
||||||
position: position,
|
|
||||||
items: [
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'delete',
|
|
||||||
child: Text(
|
|
||||||
widget.options.translations.deleteReaction,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (value == 'delete') {
|
|
||||||
// Call service to delete reaction
|
|
||||||
updatePost(
|
|
||||||
await widget.service.postService
|
|
||||||
.deletePostReaction(post, reaction.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (reaction.creator?.imageUrl != null &&
|
|
||||||
reaction.creator!.imageUrl!.isNotEmpty) ...[
|
|
||||||
widget.options.userAvatarBuilder?.call(
|
|
||||||
reaction.creator!,
|
|
||||||
14,
|
|
||||||
) ??
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
|
||||||
reaction.creator!.imageUrl!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
widget.options.anonymousAvatarBuilder?.call(
|
|
||||||
reaction.creator!,
|
|
||||||
14,
|
|
||||||
) ??
|
|
||||||
const CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
if (reaction.imageUrl != null) ...[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.options.nameBuilder
|
|
||||||
?.call(reaction.creator) ??
|
|
||||||
reaction.creator?.fullName ??
|
|
||||||
widget.options.translations
|
|
||||||
.anonymousUser,
|
|
||||||
style: theme.textTheme.titleSmall!
|
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: reaction.imageUrl!,
|
|
||||||
fit: BoxFit.fitWidth,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
Expanded(
|
|
||||||
child: Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
text: widget.options.nameBuilder
|
|
||||||
?.call(reaction.creator) ??
|
|
||||||
reaction.creator?.fullName ??
|
|
||||||
widget
|
|
||||||
.options.translations.anonymousUser,
|
|
||||||
style: theme.textTheme.titleSmall!
|
|
||||||
.copyWith(color: Colors.black),
|
|
||||||
children: [
|
|
||||||
const TextSpan(text: ' '),
|
|
||||||
TextSpan(
|
|
||||||
text: reaction.reaction ?? '',
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: dateFormat
|
|
||||||
.format(reaction.createdAt),
|
|
||||||
style: theme.textTheme.labelSmall!
|
|
||||||
.copyWith(
|
|
||||||
color: theme
|
|
||||||
.textTheme.labelSmall!.color!
|
|
||||||
.withOpacity(0.5),
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// text should go to new line
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
var isLikedByUser =
|
|
||||||
reaction.likedBy?.contains(widget.userId) ??
|
|
||||||
false;
|
|
||||||
return IconButton(
|
|
||||||
padding: const EdgeInsets.only(left: 12),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: () async {
|
|
||||||
if (isLikedByUser) {
|
|
||||||
updatePost(
|
|
||||||
await widget.service.postService
|
|
||||||
.unlikeReaction(
|
|
||||||
widget.userId,
|
|
||||||
post,
|
|
||||||
reaction.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
} else {
|
|
||||||
updatePost(
|
|
||||||
await widget.service.postService
|
|
||||||
.likeReaction(
|
|
||||||
widget.userId,
|
|
||||||
post,
|
|
||||||
reaction.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: isLikedByUser
|
|
||||||
? widget.options.theme.likedIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_rounded,
|
|
||||||
color:
|
|
||||||
widget.options.theme.iconColor,
|
|
||||||
size: 14,
|
|
||||||
)
|
|
||||||
: widget.options.theme.likeIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.favorite_outline_outlined,
|
|
||||||
color:
|
|
||||||
widget.options.theme.iconColor,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
],
|
|
||||||
if (post.reactions?.isEmpty ?? true) ...[
|
|
||||||
Text(
|
|
||||||
widget.options.translations.firstComment,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 120),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (post.reactionEnabled && !(widget.isOverviewScreen ?? false))
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Container(
|
|
||||||
color: theme.scaffoldBackgroundColor,
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: MediaQuery.of(context).size.width,
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
|
||||||
bottom: true,
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8),
|
|
||||||
child: post.creator!.imageUrl != null
|
|
||||||
? widget.options.userAvatarBuilder?.call(
|
|
||||||
post.creator!,
|
|
||||||
28,
|
|
||||||
) ??
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
|
||||||
post.creator!.imageUrl!,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: widget.options.anonymousAvatarBuilder?.call(
|
|
||||||
post.creator!,
|
|
||||||
28,
|
|
||||||
) ??
|
|
||||||
const CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 8,
|
|
||||||
right: 16,
|
|
||||||
top: 8,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
child: ReactionBottom(
|
|
||||||
messageInputBuilder: textInputBuilder,
|
|
||||||
onReactionSubmit: (reaction) async => updatePost(
|
|
||||||
await widget.service.postService.reactToPost(
|
|
||||||
post,
|
|
||||||
TimelinePostReaction(
|
|
||||||
id: '',
|
|
||||||
postId: post.id,
|
|
||||||
reaction: reaction,
|
|
||||||
creatorId: widget.userId,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
translations: widget.options.translations,
|
|
||||||
iconColor: widget.options.theme.iconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,343 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
|
||||||
|
|
||||||
class TimelineScreen extends StatefulWidget {
|
|
||||||
const TimelineScreen({
|
|
||||||
this.userId = 'test_user',
|
|
||||||
this.service,
|
|
||||||
this.options = const TimelineOptions(),
|
|
||||||
this.onPostTap,
|
|
||||||
this.scrollController,
|
|
||||||
this.onUserTap,
|
|
||||||
this.onRefresh,
|
|
||||||
this.posts,
|
|
||||||
this.timelineCategory,
|
|
||||||
this.postWidgetBuilder,
|
|
||||||
this.filterEnabled = false,
|
|
||||||
this.allowAllDeletion = false,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The user id of the current user
|
|
||||||
final String userId;
|
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
|
||||||
/// only the posts of the current user
|
|
||||||
final bool allowAllDeletion;
|
|
||||||
|
|
||||||
/// The service to use for fetching and manipulating posts
|
|
||||||
final TimelineService? service;
|
|
||||||
|
|
||||||
/// All the configuration options for the timelinescreens and widgets
|
|
||||||
final TimelineOptions options;
|
|
||||||
|
|
||||||
/// The controller for the scroll view
|
|
||||||
final ScrollController? scrollController;
|
|
||||||
|
|
||||||
/// The string to filter the timeline by category
|
|
||||||
final String? timelineCategory;
|
|
||||||
|
|
||||||
/// This is used if you want to pass in a list of posts instead
|
|
||||||
/// of fetching them from the service
|
|
||||||
final List<TimelinePost>? posts;
|
|
||||||
|
|
||||||
/// Called when a post is tapped
|
|
||||||
final Function(TimelinePost)? onPostTap;
|
|
||||||
|
|
||||||
/// Called when the timeline is refreshed by pulling down
|
|
||||||
final Function(BuildContext context, String? category)? onRefresh;
|
|
||||||
|
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
|
||||||
final Function(String userId)? onUserTap;
|
|
||||||
|
|
||||||
/// Override the standard postwidget
|
|
||||||
final Widget Function(TimelinePost post)? postWidgetBuilder;
|
|
||||||
|
|
||||||
/// if true the filter textfield is enabled.
|
|
||||||
final bool filterEnabled;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TimelineScreen> createState() => _TimelineScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineScreenState extends State<TimelineScreen> {
|
|
||||||
late ScrollController controller;
|
|
||||||
late var textFieldController = TextEditingController(
|
|
||||||
text: widget.options.filterOptions.initialFilterWord,
|
|
||||||
);
|
|
||||||
late var service = widget.service ??
|
|
||||||
TimelineService(
|
|
||||||
postService: LocalTimelinePostService(),
|
|
||||||
);
|
|
||||||
|
|
||||||
bool isLoading = true;
|
|
||||||
|
|
||||||
late var category = widget.timelineCategory;
|
|
||||||
|
|
||||||
late var filterWord = widget.options.filterOptions.initialFilterWord;
|
|
||||||
|
|
||||||
bool _isOnTop = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
controller.removeListener(_updateIsOnTop);
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateIsOnTop() {
|
|
||||||
setState(() {
|
|
||||||
_isOnTop = controller.position.pixels < 0.1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = widget.scrollController ?? ScrollController();
|
|
||||||
controller.addListener(_updateIsOnTop);
|
|
||||||
|
|
||||||
// only load the posts after the first frame
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
unawaited(loadPosts());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (isLoading && widget.posts == null) {
|
|
||||||
return const Center(child: CircularProgressIndicator.adaptive());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the list of posts
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: service.postService,
|
|
||||||
builder: (context, _) {
|
|
||||||
if (!context.mounted) return const SizedBox();
|
|
||||||
var posts = widget.posts ?? service.postService.getPosts(category);
|
|
||||||
|
|
||||||
if (widget.filterEnabled && filterWord != null) {
|
|
||||||
if (service.postService is TimelineFilterService) {
|
|
||||||
posts = (service.postService as TimelineFilterService)
|
|
||||||
.filterPosts(filterWord!, {});
|
|
||||||
} else {
|
|
||||||
debugPrint('Timeline service needs to mixin'
|
|
||||||
' with TimelineFilterService');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = posts
|
|
||||||
.where(
|
|
||||||
(p) => category == null || p.category == category,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// sort posts by date
|
|
||||||
if (widget.options.sortPostsAscending != null) {
|
|
||||||
posts.sort(
|
|
||||||
(a, b) => widget.options.sortPostsAscending!
|
|
||||||
? a.createdAt.compareTo(b.createdAt)
|
|
||||||
: b.createdAt.compareTo(a.createdAt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var categories = service.postService.categories;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: widget.options.paddings.mainPadding.top,
|
|
||||||
),
|
|
||||||
if (widget.filterEnabled) ...[
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: widget.options.paddings.mainPadding.left,
|
|
||||||
right: widget.options.paddings.mainPadding.right,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: textFieldController,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
filterWord = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: widget.options.translations.searchHint,
|
|
||||||
suffixIconConstraints:
|
|
||||||
const BoxConstraints(maxHeight: 14),
|
|
||||||
contentPadding: const EdgeInsets.only(
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
bottom: -10,
|
|
||||||
),
|
|
||||||
suffixIcon: const Padding(
|
|
||||||
padding: EdgeInsets.only(right: 12),
|
|
||||||
child: Icon(Icons.search),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
textFieldController.clear();
|
|
||||||
filterWord = null;
|
|
||||||
widget.options.filterOptions.onFilterEnabledChange
|
|
||||||
?.call(filterEnabled: false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.all(8),
|
|
||||||
child: Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Color(0xFF000000),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
CategorySelector(
|
|
||||||
categories: categories,
|
|
||||||
isOnTop: _isOnTop,
|
|
||||||
filter: category,
|
|
||||||
options: widget.options,
|
|
||||||
onTapCategory: (categoryKey) {
|
|
||||||
setState(() {
|
|
||||||
service.postService.selectedCategory =
|
|
||||||
categories.firstWhereOrNull(
|
|
||||||
(element) => element.key == categoryKey,
|
|
||||||
);
|
|
||||||
category = categoryKey;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 12,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: RefreshIndicator.adaptive(
|
|
||||||
onRefresh: () async {
|
|
||||||
await widget.onRefresh?.call(context, category);
|
|
||||||
await loadPosts();
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: controller,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
/// Add a optional custom header to the list of posts
|
|
||||||
widget.options.listHeaderBuilder
|
|
||||||
?.call(context, category) ??
|
|
||||||
const SizedBox.shrink(),
|
|
||||||
...posts.map(
|
|
||||||
(post) => Padding(
|
|
||||||
padding: widget.options.paddings.postPadding,
|
|
||||||
child: widget.postWidgetBuilder?.call(post) ??
|
|
||||||
TimelinePostWidget(
|
|
||||||
service: service,
|
|
||||||
userId: widget.userId,
|
|
||||||
options: widget.options,
|
|
||||||
allowAllDeletion: widget.allowAllDeletion,
|
|
||||||
post: post,
|
|
||||||
onTap: () async {
|
|
||||||
if (widget.onPostTap != null) {
|
|
||||||
widget.onPostTap!.call(post);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => Scaffold(
|
|
||||||
body: TimelinePostScreen(
|
|
||||||
userId: 'test_user',
|
|
||||||
service: service,
|
|
||||||
options: widget.options,
|
|
||||||
post: post,
|
|
||||||
onPostDelete: () {
|
|
||||||
service.postService
|
|
||||||
.deletePost(post);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onTapLike: () async => service.postService
|
|
||||||
.likePost(widget.userId, post),
|
|
||||||
onTapUnlike: () async => service.postService
|
|
||||||
.unlikePost(widget.userId, post),
|
|
||||||
onPostDelete: () async =>
|
|
||||||
service.postService.deletePost(post),
|
|
||||||
onUserTap: widget.onUserTap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (posts.isEmpty)
|
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
category == null
|
|
||||||
? widget.options.translations.noPosts
|
|
||||||
: widget
|
|
||||||
.options.translations.noPostsWithFilter,
|
|
||||||
style:
|
|
||||||
widget.options.theme.textStyles.noPostsStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: widget.options.paddings.mainPadding.bottom,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadPosts() async {
|
|
||||||
if (widget.posts != null || !context.mounted) return;
|
|
||||||
try {
|
|
||||||
await service.postService.fetchCategories();
|
|
||||||
await service.postService.fetchPosts(category);
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
} on Exception catch (e) {
|
|
||||||
// Handle errors here
|
|
||||||
debugPrint('Error loading posts: $e');
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,215 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/post_creation_textfield.dart';
|
|
||||||
|
|
||||||
class TimelineSelectionScreen extends StatefulWidget {
|
|
||||||
const TimelineSelectionScreen({
|
|
||||||
required this.options,
|
|
||||||
required this.categories,
|
|
||||||
required this.onCategorySelected,
|
|
||||||
required this.postService,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<TimelineCategory> categories;
|
|
||||||
|
|
||||||
final TimelineOptions options;
|
|
||||||
|
|
||||||
final Function(TimelineCategory) onCategorySelected;
|
|
||||||
|
|
||||||
final TimelinePostService postService;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TimelineSelectionScreen> createState() =>
|
|
||||||
_TimelineSelectionScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var size = MediaQuery.of(context).size;
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: size.width * 0.05,
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 20, bottom: 12),
|
|
||||||
child: Text(
|
|
||||||
widget.options.translations.timelineSelectionDescription,
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (var category in widget.categories.where(
|
|
||||||
(element) => element.canCreate && element.key != null,
|
|
||||||
)) ...[
|
|
||||||
widget.options.categorySelectorButtonBuilder?.call(
|
|
||||||
context,
|
|
||||||
() {
|
|
||||||
widget.onCategorySelected.call(category);
|
|
||||||
},
|
|
||||||
category.title,
|
|
||||||
) ??
|
|
||||||
InkWell(
|
|
||||||
onTap: () => widget.onCategorySelected.call(category),
|
|
||||||
child: Container(
|
|
||||||
height: 60,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: widget.options.theme
|
|
||||||
.categorySelectionButtonBorderColor ??
|
|
||||||
Theme.of(context).primaryColor,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
color: widget.options.theme
|
|
||||||
.categorySelectionButtonBackgroundColor,
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
child: Text(
|
|
||||||
category.title,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
InkWell(
|
|
||||||
onTap: showCategoryPopup,
|
|
||||||
child: Container(
|
|
||||||
height: 60,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: widget
|
|
||||||
.options.theme.categorySelectionButtonBorderColor ??
|
|
||||||
const Color(0xFF9E9E9E),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
color: widget
|
|
||||||
.options.theme.categorySelectionButtonBackgroundColor,
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: theme.textTheme.titleMedium?.color!
|
|
||||||
.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.addCategoryTitle,
|
|
||||||
style: theme.textTheme.titleMedium!.copyWith(
|
|
||||||
color: theme.textTheme.titleMedium?.color!
|
|
||||||
.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> showCategoryPopup() async {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var controller = TextEditingController();
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
|
||||||
insetPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
contentPadding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 50, vertical: 24),
|
|
||||||
titlePadding: const EdgeInsets.only(left: 44, right: 44, top: 32),
|
|
||||||
title: Text(
|
|
||||||
widget.options.translations.createCategoryPopuptitle,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
PostCreationTextfield(
|
|
||||||
controller: controller,
|
|
||||||
hintText: widget.options.translations.addCategoryHintText,
|
|
||||||
validator: (p0) => p0!.isEmpty
|
|
||||||
? widget.options.translations.addCategoryErrorText
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
||||||
child: DefaultFilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
if (controller.text.isEmpty) return;
|
|
||||||
await widget.postService.addCategory(
|
|
||||||
TimelineCategory(
|
|
||||||
key: controller.text,
|
|
||||||
title: controller.text,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
if (context.mounted) Navigator.pop(context);
|
|
||||||
},
|
|
||||||
buttonText:
|
|
||||||
widget.options.translations.addCategorySubmitButton,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(false);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
widget.options.translations.addCategoryCancelButtton,
|
|
||||||
style: theme.textTheme.bodyMedium!.copyWith(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
color: theme.textTheme.bodyMedium?.color!.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,332 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
|
|
||||||
class LocalTimelinePostService
|
|
||||||
with ChangeNotifier
|
|
||||||
implements TimelinePostService {
|
|
||||||
@override
|
|
||||||
List<TimelinePost> posts = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelineCategory> categories = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimelineCategory? selectedCategory;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
|
||||||
posts.add(
|
|
||||||
post.copyWith(
|
|
||||||
creator: const TimelinePosterUserModel(
|
|
||||||
userId: 'test_user',
|
|
||||||
imageUrl:
|
|
||||||
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
|
||||||
firstName: 'Ico',
|
|
||||||
lastName: 'Nica',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
notifyListeners();
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> deletePost(TimelinePost post) async {
|
|
||||||
posts = posts.where((element) => element.id != post.id).toList();
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> deletePostReaction(
|
|
||||||
TimelinePost post,
|
|
||||||
String reactionId,
|
|
||||||
) async {
|
|
||||||
if (post.reactions != null && post.reactions!.isNotEmpty) {
|
|
||||||
var reaction =
|
|
||||||
post.reactions!.firstWhere((element) => element.id == reactionId);
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
reaction: post.reaction - 1,
|
|
||||||
reactions: (post.reactions ?? [])..remove(reaction),
|
|
||||||
);
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> fetchPostDetails(TimelinePost post) async {
|
|
||||||
var reactions = post.reactions ?? [];
|
|
||||||
var updatedReactions = <TimelinePostReaction>[];
|
|
||||||
for (var reaction in reactions) {
|
|
||||||
updatedReactions.add(
|
|
||||||
reaction.copyWith(
|
|
||||||
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',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var updatedPost = post.copyWith(reactions: updatedReactions);
|
|
||||||
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
|
||||||
if (posts.isEmpty) {
|
|
||||||
posts = getMockedPosts();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
return posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<TimelinePost>> fetchPostsPaginated(
|
|
||||||
String? category,
|
|
||||||
int limit,
|
|
||||||
) async {
|
|
||||||
notifyListeners();
|
|
||||||
return posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> fetchPost(TimelinePost post) async {
|
|
||||||
notifyListeners();
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
|
||||||
var newPosts = <TimelinePost>[];
|
|
||||||
|
|
||||||
posts = [...posts, ...newPosts];
|
|
||||||
notifyListeners();
|
|
||||||
return posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost?> getPost(String postId) => Future.value(
|
|
||||||
(posts.any((element) => element.id == postId))
|
|
||||||
? posts.firstWhere((element) => element.id == postId)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<TimelinePost> getPosts(String? category) => posts
|
|
||||||
.where((element) => category == null || element.category == category)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> likePost(String userId, TimelinePost post) async {
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
likes: post.likes + 1,
|
|
||||||
likedBy: (post.likedBy ?? [])..add(userId),
|
|
||||||
);
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> unlikePost(String userId, TimelinePost post) async {
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
likes: post.likes - 1,
|
|
||||||
likedBy: post.likedBy?..remove(userId),
|
|
||||||
);
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<TimelinePost> reactToPost(
|
|
||||||
TimelinePost post,
|
|
||||||
TimelinePostReaction reaction, {
|
|
||||||
Uint8List? image,
|
|
||||||
}) async {
|
|
||||||
var reactionId = DateTime.now().millisecondsSinceEpoch.toString();
|
|
||||||
|
|
||||||
var updatedReaction = reaction.copyWith(
|
|
||||||
id: reactionId,
|
|
||||||
creator: const TimelinePosterUserModel(
|
|
||||||
userId: 'test_user',
|
|
||||||
imageUrl:
|
|
||||||
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
|
|
||||||
firstName: 'Ico',
|
|
||||||
lastName: 'Nica',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var updatedPost = post.copyWith(
|
|
||||||
reaction: post.reaction + 1,
|
|
||||||
reactions: post.reactions?..add(updatedReaction),
|
|
||||||
);
|
|
||||||
|
|
||||||
posts = posts
|
|
||||||
.map(
|
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
notifyListeners();
|
|
||||||
return updatedPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TimelinePost> getMockedPosts() => [
|
|
||||||
TimelinePost(
|
|
||||||
id: 'Post0',
|
|
||||||
creatorId: 'test_user',
|
|
||||||
title: 'De topper van de maand september',
|
|
||||||
category: 'Category',
|
|
||||||
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',
|
|
||||||
content: 'Dit is onze topper van de maand september! Gefeliciteerd!',
|
|
||||||
likes: 72,
|
|
||||||
reaction: 0,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
reactionEnabled: true,
|
|
||||||
creator: const TimelinePosterUserModel(
|
|
||||||
userId: 'test_user',
|
|
||||||
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',
|
|
||||||
firstName: 'Robin',
|
|
||||||
lastName: 'De Vries',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TimelinePost(
|
|
||||||
id: 'Post1',
|
|
||||||
creatorId: 'test_user2',
|
|
||||||
title: 'De soep van de week is: Aspergesoep',
|
|
||||||
category: 'Category with two lines',
|
|
||||||
content:
|
|
||||||
'Aspergesoep is echt een heerlijke delicatesse. Deze soep wordt'
|
|
||||||
' vaak gemaakt met verse asperges, bouillon en wat kruiden voor'
|
|
||||||
' smaak. Het is een perfecte keuze voor een lichte en smaakvolle'
|
|
||||||
' maaltijd, vooral in het voorjaar wanneer asperges in seizoen'
|
|
||||||
' zijn. We serveren het met een vleugje room en wat knapperige'
|
|
||||||
' croutons voor die extra touch.',
|
|
||||||
likes: 72,
|
|
||||||
reaction: 0,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
reactionEnabled: true,
|
|
||||||
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',
|
|
||||||
creator: const TimelinePosterUserModel(
|
|
||||||
userId: 'test_user',
|
|
||||||
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',
|
|
||||||
firstName: 'Elise',
|
|
||||||
lastName: 'Welling',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
@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,67 +0,0 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
|
||||||
|
|
||||||
class CategorySelector extends StatefulWidget {
|
|
||||||
const CategorySelector({
|
|
||||||
required this.filter,
|
|
||||||
required this.options,
|
|
||||||
required this.onTapCategory,
|
|
||||||
required this.isOnTop,
|
|
||||||
required this.categories,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String? filter;
|
|
||||||
final TimelineOptions options;
|
|
||||||
final void Function(String? categoryKey) onTapCategory;
|
|
||||||
final bool isOnTop;
|
|
||||||
final List<TimelineCategory> categories;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CategorySelector> createState() => _CategorySelectorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CategorySelectorState extends State<CategorySelector> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: widget.options.categoriesOptions
|
|
||||||
.categorySelectorHorizontalPadding ??
|
|
||||||
max(widget.options.paddings.mainPadding.left - 20, 0),
|
|
||||||
),
|
|
||||||
for (var category in widget.categories) ...[
|
|
||||||
widget.options.categoriesOptions.categoryButtonBuilder?.call(
|
|
||||||
category,
|
|
||||||
() => widget.onTapCategory(category.key),
|
|
||||||
widget.filter == category.key,
|
|
||||||
widget.isOnTop,
|
|
||||||
) ??
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
child: CategorySelectorButton(
|
|
||||||
isOnTop: widget.isOnTop,
|
|
||||||
category: category,
|
|
||||||
selected: widget.filter == category.key,
|
|
||||||
onTap: () => widget.onTapCategory(category.key),
|
|
||||||
options: widget.options,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
SizedBox(
|
|
||||||
width: widget.options.categoriesOptions
|
|
||||||
.categorySelectorHorizontalPadding ??
|
|
||||||
max(widget.options.paddings.mainPadding.right - 4, 0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
|
||||||
|
|
||||||
class CategorySelectorButton extends StatelessWidget {
|
|
||||||
const CategorySelectorButton({
|
|
||||||
required this.category,
|
|
||||||
required this.selected,
|
|
||||||
required this.onTap,
|
|
||||||
required this.options,
|
|
||||||
required this.isOnTop,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TimelineCategory category;
|
|
||||||
final bool selected;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final TimelineOptions options;
|
|
||||||
final bool isOnTop;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var size = MediaQuery.of(context).size;
|
|
||||||
|
|
||||||
return AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
height: isOnTop ? 140 : 40,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: onTap,
|
|
||||||
style: ButtonStyle(
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
padding: const WidgetStatePropertyAll(
|
|
||||||
EdgeInsets.symmetric(
|
|
||||||
vertical: 5,
|
|
||||||
horizontal: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
fixedSize: WidgetStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
|
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
|
||||||
selected
|
|
||||||
? theme.colorScheme.primary
|
|
||||||
: options.theme.categorySelectionButtonBackgroundColor ??
|
|
||||||
Colors.transparent,
|
|
||||||
),
|
|
||||||
shape: WidgetStatePropertyAll(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
side: BorderSide(
|
|
||||||
color: options.theme.categorySelectionButtonBorderColor ??
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: isOnTop
|
|
||||||
? SizedBox(
|
|
||||||
width: size.width,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_CategoryButtonText(
|
|
||||||
category: category,
|
|
||||||
options: options,
|
|
||||||
theme: theme,
|
|
||||||
selected: selected,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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,58 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
|
||||||
|
|
||||||
class ReactionBottom extends StatefulWidget {
|
|
||||||
const ReactionBottom({
|
|
||||||
required this.onReactionSubmit,
|
|
||||||
required this.messageInputBuilder,
|
|
||||||
required this.translations,
|
|
||||||
this.iconColor,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Future<void> Function(String text) onReactionSubmit;
|
|
||||||
final TextInputBuilder messageInputBuilder;
|
|
||||||
final TimelineTranslations translations;
|
|
||||||
final Color? iconColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ReactionBottom> createState() => _ReactionBottomState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReactionBottomState extends State<ReactionBottom> {
|
|
||||||
final TextEditingController _textEditingController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Container(
|
|
||||||
child: widget.messageInputBuilder(
|
|
||||||
_textEditingController,
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
var value = _textEditingController.text;
|
|
||||||
if (value.isNotEmpty) {
|
|
||||||
await widget.onReactionSubmit(value);
|
|
||||||
_textEditingController.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: SvgPicture.asset(
|
|
||||||
'assets/send.svg',
|
|
||||||
package: 'flutter_timeline_view',
|
|
||||||
// ignore: deprecated_member_use
|
|
||||||
color: widget.iconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
widget.translations.writeComment,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,444 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart';
|
|
||||||
import 'package:flutter_timeline_view/src/widgets/tappable_image.dart';
|
|
||||||
|
|
||||||
class TimelinePostWidget extends StatefulWidget {
|
|
||||||
const TimelinePostWidget({
|
|
||||||
required this.userId,
|
|
||||||
required this.options,
|
|
||||||
required this.post,
|
|
||||||
required this.onTap,
|
|
||||||
required this.onTapLike,
|
|
||||||
required this.onTapUnlike,
|
|
||||||
required this.onPostDelete,
|
|
||||||
required this.service,
|
|
||||||
required this.allowAllDeletion,
|
|
||||||
this.onUserTap,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The user id of the current user
|
|
||||||
final String userId;
|
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
|
||||||
/// only the posts of the current user
|
|
||||||
final bool allowAllDeletion;
|
|
||||||
|
|
||||||
final TimelineOptions options;
|
|
||||||
|
|
||||||
final TimelinePost post;
|
|
||||||
|
|
||||||
/// Optional max height of the post
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final VoidCallback onTapLike;
|
|
||||||
final VoidCallback onTapUnlike;
|
|
||||||
final VoidCallback onPostDelete;
|
|
||||||
final TimelineService service;
|
|
||||||
|
|
||||||
/// If this is not null, the user can tap on the user avatar or name
|
|
||||||
final Function(String userId)? onUserTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TimelinePostWidget> createState() => _TimelinePostWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false;
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: widget.post.imageUrl != null || widget.post.image != null
|
|
||||||
? widget.options.postWidgetHeight
|
|
||||||
: null,
|
|
||||||
width: double.infinity,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (widget.post.creator != null) ...[
|
|
||||||
InkWell(
|
|
||||||
onTap: widget.onUserTap != null
|
|
||||||
? () =>
|
|
||||||
widget.onUserTap?.call(widget.post.creator!.userId)
|
|
||||||
: null,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (widget.post.creator!.imageUrl != null) ...[
|
|
||||||
widget.options.userAvatarBuilder?.call(
|
|
||||||
widget.post.creator!,
|
|
||||||
28,
|
|
||||||
) ??
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
|
||||||
widget.post.creator!.imageUrl!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
widget.options.anonymousAvatarBuilder?.call(
|
|
||||||
widget.post.creator!,
|
|
||||||
28,
|
|
||||||
) ??
|
|
||||||
const CircleAvatar(
|
|
||||||
radius: 14,
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(
|
|
||||||
widget.options.nameBuilder?.call(widget.post.creator) ??
|
|
||||||
widget.post.creator?.fullName ??
|
|
||||||
widget.options.translations.anonymousUser,
|
|
||||||
style: widget.options.theme.textStyles
|
|
||||||
.postCreatorTitleStyle ??
|
|
||||||
theme.textTheme.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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: widget.options.theme.moreIcon ??
|
|
||||||
Icon(
|
|
||||||
Icons.more_horiz_rounded,
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// image of the post
|
|
||||||
if (widget.post.imageUrl != null || widget.post.image != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Flexible(
|
|
||||||
flex: widget.options.postWidgetHeight != null ? 1 : 0,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: widget.options.doubleTapTolike
|
|
||||||
? TappableImage(
|
|
||||||
likeAndDislikeIcon:
|
|
||||||
widget.options.likeAndDislikeIconsForDoubleTap,
|
|
||||||
post: widget.post,
|
|
||||||
userId: widget.userId,
|
|
||||||
onLike: ({required bool liked}) async {
|
|
||||||
var userId = widget.userId;
|
|
||||||
|
|
||||||
late TimelinePost result;
|
|
||||||
|
|
||||||
if (!liked) {
|
|
||||||
result = await widget.service.postService.likePost(
|
|
||||||
userId,
|
|
||||||
widget.post,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
result =
|
|
||||||
await widget.service.postService.unlikePost(
|
|
||||||
userId,
|
|
||||||
widget.post,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.likedBy?.contains(userId) ?? false;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: widget.post.imageUrl != null
|
|
||||||
? CachedNetworkImage(
|
|
||||||
width: double.infinity,
|
|
||||||
imageUrl: widget.post.imageUrl!,
|
|
||||||
fit: BoxFit.fitWidth,
|
|
||||||
)
|
|
||||||
: Image.memory(
|
|
||||||
width: double.infinity,
|
|
||||||
widget.post.image!,
|
|
||||||
fit: BoxFit.fitWidth,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
// post information
|
|
||||||
if (widget.options.iconsWithValues) ...[
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: () async {
|
|
||||||
var userId = widget.userId;
|
|
||||||
|
|
||||||
if (!isLikedByUser) {
|
|
||||||
await widget.service.postService.likePost(
|
|
||||||
userId,
|
|
||||||
widget.post,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await widget.service.postService.unlikePost(
|
|
||||||
userId,
|
|
||||||
widget.post,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: widget.options.theme.likeIcon ??
|
|
||||||
Icon(
|
|
||||||
isLikedByUser
|
|
||||||
? Icons.favorite_rounded
|
|
||||||
: Icons.favorite_outline_outlined,
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
size: widget.options.iconSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Text('${widget.post.likes}'),
|
|
||||||
if (widget.post.reactionEnabled) ...[
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: widget.onTap,
|
|
||||||
icon: widget.options.theme.commentIcon ??
|
|
||||||
SvgPicture.asset(
|
|
||||||
'assets/Comment.svg',
|
|
||||||
package: 'flutter_timeline_view',
|
|
||||||
// ignore: deprecated_member_use
|
|
||||||
color: widget.options.theme.iconColor,
|
|
||||||
width: widget.options.iconSize,
|
|
||||||
height: widget.options.iconSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Text('${widget.post.reaction}'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
] 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: 4),
|
|
||||||
InkWell(
|
|
||||||
onTap: widget.onTap,
|
|
||||||
child: Text(
|
|
||||||
widget.options.translations.viewPost,
|
|
||||||
style: widget.options.theme.textStyles.viewPostStyle ??
|
|
||||||
theme.textTheme.titleSmall!.copyWith(
|
|
||||||
color: const Color(0xFF8D8D8D),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: flutter_timeline_view
|
|
||||||
description: Visual elements of the Flutter Timeline Component
|
|
||||||
version: 5.1.0
|
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ">=3.1.3 <4.0.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
intl: any
|
|
||||||
cached_network_image: ^3.2.2
|
|
||||||
dotted_border: ^2.1.0
|
|
||||||
collection: any
|
|
||||||
flutter_svg: ^2.0.10+1
|
|
||||||
flutter_timeline_interface:
|
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
version: ^5.1.0
|
|
||||||
flutter_image_picker:
|
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
|
||||||
version: ^4.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_lints: ^2.0.0
|
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 6.0.0
|
|
||||||
|
|
||||||
flutter:
|
|
||||||
assets:
|
|
||||||
- assets/
|
|
29
packages/timeline_repository_interface/.gitignore
vendored
Normal file
29
packages/timeline_repository_interface/.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
1
packages/timeline_repository_interface/CONTRIBUTING.md
Symbolic link
1
packages/timeline_repository_interface/CONTRIBUTING.md
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../CONTRIBUTING.md
|
|
@ -0,0 +1,7 @@
|
||||||
|
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
|
@ -0,0 +1,10 @@
|
||||||
|
import "package:timeline_repository_interface/src/models/timeline_category.dart";
|
||||||
|
|
||||||
|
abstract class CategoryRepositoryInterface {
|
||||||
|
// everything is done with streams
|
||||||
|
Stream<List<TimelineCategory>> getCategories();
|
||||||
|
Future<void> createCategory(TimelineCategory category);
|
||||||
|
TimelineCategory? selectCategory(String? categoryId);
|
||||||
|
TimelineCategory? getSelectedCategory();
|
||||||
|
TimelineCategory? getCategory(String? categoryId);
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import "dart:typed_data";
|
||||||
|
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
abstract class PostRepositoryInterface {
|
||||||
|
Stream<List<TimelinePost>> getPosts(String? categoryId);
|
||||||
|
Future<void> deletePost(String id);
|
||||||
|
//like post
|
||||||
|
Future<void> likePost(String postId, String userId);
|
||||||
|
Future<void> unlikePost(String postId, String userId);
|
||||||
|
Future<void> likePostReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction,
|
||||||
|
String userId,
|
||||||
|
);
|
||||||
|
Future<void> unlikePostReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction,
|
||||||
|
String userId,
|
||||||
|
);
|
||||||
|
Future<void> createReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction, {
|
||||||
|
Uint8List? image,
|
||||||
|
});
|
||||||
|
|
||||||
|
void setCurrentPost(TimelinePost post);
|
||||||
|
TimelinePost getCurrentPost();
|
||||||
|
Future<void> createPost(TimelinePost post);
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import "package:timeline_repository_interface/src/models/timeline_user.dart";
|
||||||
|
|
||||||
|
abstract class TimelineUserRepositoryInterface {
|
||||||
|
Future<List<TimelineUser>> getAllUsers();
|
||||||
|
Future<TimelineUser> getCurrentUser();
|
||||||
|
Future<TimelineUser?> getUser(String userId);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import "package:timeline_repository_interface/timeline_repository_interface.dart";
|
||||||
|
|
||||||
|
class LocalCategoryRepository implements CategoryRepositoryInterface {
|
||||||
|
final List<TimelineCategory> _categories = [
|
||||||
|
const TimelineCategory(key: null, title: "All"),
|
||||||
|
const TimelineCategory(key: "1", title: "Category"),
|
||||||
|
const TimelineCategory(key: "2", title: "Category with two lines"),
|
||||||
|
];
|
||||||
|
|
||||||
|
TimelineCategory? _selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createCategory(TimelineCategory category) async {
|
||||||
|
_categories.add(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<TimelineCategory>> getCategories() => Stream.value(_categories);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineCategory selectCategory(String? categoryId) {
|
||||||
|
_selectedCategory = _categories.firstWhere(
|
||||||
|
(category) => category.key == categoryId,
|
||||||
|
orElse: () => _categories.first,
|
||||||
|
);
|
||||||
|
return _selectedCategory!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineCategory? getSelectedCategory() => _selectedCategory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelineCategory? getCategory(String? categoryId) => _categories.firstWhere(
|
||||||
|
(category) => category.key == categoryId,
|
||||||
|
orElse: () => _categories.first,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
import "dart:async";
|
||||||
|
import "dart:typed_data";
|
||||||
|
|
||||||
|
import "package:rxdart/rxdart.dart";
|
||||||
|
import "package:timeline_repository_interface/src/interfaces/post_repository_interface.dart";
|
||||||
|
import "package:timeline_repository_interface/src/models/timeline_post.dart";
|
||||||
|
import "package:timeline_repository_interface/src/models/timeline_post_reaction.dart";
|
||||||
|
import "package:timeline_repository_interface/src/models/timeline_user.dart";
|
||||||
|
|
||||||
|
class LocalPostRepository implements PostRepositoryInterface {
|
||||||
|
LocalPostRepository();
|
||||||
|
|
||||||
|
final StreamController<List<TimelinePost>> _postsController =
|
||||||
|
BehaviorSubject<List<TimelinePost>>();
|
||||||
|
|
||||||
|
late TimelinePost? _currentPost;
|
||||||
|
|
||||||
|
final jane = const TimelineUser(
|
||||||
|
userId: "1",
|
||||||
|
firstName: "Jane",
|
||||||
|
lastName: "Doe",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<TimelinePost> _posts = List.generate(
|
||||||
|
10,
|
||||||
|
(index) => TimelinePost(
|
||||||
|
id: index.toString(),
|
||||||
|
creatorId: "1",
|
||||||
|
title: "test title",
|
||||||
|
content: "lore ipsum, dolor sit amet, consectetur adipiscing elit,"
|
||||||
|
" sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
likes: 50,
|
||||||
|
reaction: 5,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
reactionEnabled: true,
|
||||||
|
category: "2",
|
||||||
|
reactions: [
|
||||||
|
TimelinePostReaction(
|
||||||
|
id: "2",
|
||||||
|
postId: index.toString(),
|
||||||
|
creatorId: "1",
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
reaction: "This is a test reaction",
|
||||||
|
likedBy: [],
|
||||||
|
creator: const TimelineUser(
|
||||||
|
userId: "2",
|
||||||
|
firstName: "John",
|
||||||
|
lastName: "Doe",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
likedBy: [],
|
||||||
|
creator: const TimelineUser(
|
||||||
|
userId: "1",
|
||||||
|
firstName: "Jane",
|
||||||
|
lastName: "Doe",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
),
|
||||||
|
imageUrl: "https://via.placeholder.com/1000",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<TimelinePost>> getPosts(String? categoryId) {
|
||||||
|
if (categoryId == null) {
|
||||||
|
_postsController.add(_posts);
|
||||||
|
} else {
|
||||||
|
_postsController.add(
|
||||||
|
_posts.where((element) => element.category == categoryId).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _postsController.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deletePost(String id) async {
|
||||||
|
_posts.removeWhere((element) => element.id == id);
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> likePost(String postId, String userId) async {
|
||||||
|
var post = _posts.firstWhere((element) => element.id == postId);
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
likes: post.likes + 1,
|
||||||
|
likedBy: post.likedBy?..add(userId),
|
||||||
|
);
|
||||||
|
_posts[_posts.indexWhere((element) => element.id == postId)] = updatedPost;
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> unlikePost(String postId, String userId) async {
|
||||||
|
var post = _posts.firstWhere((element) => element.id == postId);
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
likes: post.likes - 1,
|
||||||
|
likedBy: post.likedBy?..remove(userId),
|
||||||
|
);
|
||||||
|
_posts[_posts.indexWhere((element) => element.id == postId)] = updatedPost;
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> likePostReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reaction: post.reaction + 1,
|
||||||
|
reactions: post.reactions
|
||||||
|
?..[post.reactions!
|
||||||
|
.indexWhere((element) => element.id == reaction.id)] =
|
||||||
|
reaction.copyWith(
|
||||||
|
likedBy: reaction.likedBy?..add(userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_posts[_posts.indexWhere((element) => element.id == post.id)] = updatedPost;
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> unlikePostReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reaction: post.reaction - 1,
|
||||||
|
reactions: post.reactions
|
||||||
|
?..[post.reactions!
|
||||||
|
.indexWhere((element) => element.id == reaction.id)] =
|
||||||
|
reaction.copyWith(
|
||||||
|
likedBy: reaction.likedBy?..remove(userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_posts[_posts.indexWhere((element) => element.id == post.id)] = updatedPost;
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createReaction(
|
||||||
|
TimelinePost post,
|
||||||
|
TimelinePostReaction reaction, {
|
||||||
|
Uint8List? image,
|
||||||
|
}) async {
|
||||||
|
var reactionId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
var updatedReaction = reaction.copyWith(
|
||||||
|
id: reactionId,
|
||||||
|
creator: const TimelineUser(
|
||||||
|
userId: "2",
|
||||||
|
firstName: "John",
|
||||||
|
lastName: "Doe",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reaction: post.reaction + 1,
|
||||||
|
reactions: post.reactions?..add(updatedReaction),
|
||||||
|
);
|
||||||
|
_posts[_posts.indexWhere((element) => element.id == post.id)] = updatedPost;
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setCurrentPost(TimelinePost post) async {
|
||||||
|
_currentPost = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimelinePost getCurrentPost() => _currentPost!;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createPost(TimelinePost post) async {
|
||||||
|
var postId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
id: postId,
|
||||||
|
creator: jane,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
_posts.add(updatedPost);
|
||||||
|
_postsController.add(_posts);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue