mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 18:43:45 +02:00
feat: Added search bar with filter
This commit is contained in:
parent
14dced5ef4
commit
e5e2eb5c22
13 changed files with 237 additions and 101 deletions
|
@ -22,7 +22,7 @@ class _PostScreenState extends State<PostScreen> {
|
||||||
body: TimelinePostScreen(
|
body: TimelinePostScreen(
|
||||||
userId: 'test_user',
|
userId: 'test_user',
|
||||||
service: widget.service,
|
service: widget.service,
|
||||||
options: const TimelineOptions(),
|
options: TimelineOptions(),
|
||||||
post: widget.post,
|
post: widget.post,
|
||||||
onPostDelete: () {
|
onPostDelete: () {
|
||||||
print('delete post');
|
print('delete post');
|
||||||
|
|
|
@ -11,11 +11,12 @@ import 'package:flutter_timeline/flutter_timeline.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class TestTimelineService with ChangeNotifier implements TimelineService {
|
class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
List<TimelinePost> _posts = [];
|
@override
|
||||||
|
List<TimelinePost> posts = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||||
_posts.add(
|
posts.add(
|
||||||
post.copyWith(
|
post.copyWith(
|
||||||
creator: const TimelinePosterUserModel(userId: 'test_user'),
|
creator: const TimelinePosterUserModel(userId: 'test_user'),
|
||||||
),
|
),
|
||||||
|
@ -26,7 +27,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deletePost(TimelinePost post) async {
|
Future<void> deletePost(TimelinePost post) async {
|
||||||
_posts = _posts.where((element) => element.id != post.id).toList();
|
posts = posts.where((element) => element.id != post.id).toList();
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
reaction: post.reaction - 1,
|
reaction: post.reaction - 1,
|
||||||
reactions: (post.reactions ?? [])..remove(reaction),
|
reactions: (post.reactions ?? [])..remove(reaction),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -64,7 +65,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
creator: const TimelinePosterUserModel(userId: 'test_user')));
|
creator: const TimelinePosterUserModel(userId: 'test_user')));
|
||||||
}
|
}
|
||||||
var updatedPost = post.copyWith(reactions: updatedReactions);
|
var updatedPost = post.copyWith(reactions: updatedReactions);
|
||||||
_posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +73,6 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
@override
|
@override
|
||||||
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
Future<List<TimelinePost>> fetchPosts(String? category) async {
|
||||||
var posts = getMockedPosts();
|
var posts = getMockedPosts();
|
||||||
_posts = posts;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return _posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -94,32 +94,31 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
||||||
var posts = <TimelinePost>[];
|
var newPosts = <TimelinePost>[];
|
||||||
|
|
||||||
_posts = [...posts, ..._posts];
|
posts = [...posts, ...newPosts];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TimelinePost? getPost(String postId) =>
|
TimelinePost? getPost(String postId) =>
|
||||||
(_posts.any((element) => element.id == postId))
|
(posts.any((element) => element.id == postId))
|
||||||
? _posts.firstWhere((element) => element.id == postId)
|
? posts.firstWhere((element) => element.id == postId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> getPosts(String? category) => _posts
|
List<TimelinePost> getPosts(String? category) => posts
|
||||||
.where((element) => category == null || element.category == category)
|
.where((element) => category == null || element.category == category)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost> likePost(String userId, TimelinePost post) async {
|
Future<TimelinePost> likePost(String userId, TimelinePost post) async {
|
||||||
print(userId);
|
|
||||||
var updatedPost = post.copyWith(
|
var updatedPost = post.copyWith(
|
||||||
likes: post.likes + 1,
|
likes: post.likes + 1,
|
||||||
likedBy: (post.likedBy ?? [])..add(userId),
|
likedBy: (post.likedBy ?? [])..add(userId),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -135,7 +134,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
likes: post.likes - 1,
|
likes: post.likes - 1,
|
||||||
likedBy: post.likedBy?..remove(userId),
|
likedBy: post.likedBy?..remove(userId),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -161,7 +160,7 @@ class TestTimelineService with ChangeNotifier implements TimelineService {
|
||||||
reactions: post.reactions?..add(updatedReaction),
|
reactions: post.reactions?..add(updatedReaction),
|
||||||
);
|
);
|
||||||
|
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,6 @@ List<GoRoute> getTimelineStoryRoutes(
|
||||||
options: configuration.optionsBuilder(context),
|
options: configuration.optionsBuilder(context),
|
||||||
onPostTap: (post) async =>
|
onPostTap: (post) async =>
|
||||||
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
TimelineUserStoryRoutes.timelineViewPath(post.id),
|
||||||
timelineCategoryFilter: null,
|
|
||||||
);
|
);
|
||||||
return buildScreenWithoutTransition(
|
return buildScreenWithoutTransition(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
@ -13,9 +13,7 @@ import 'package:flutter_timeline_firebase/src/models/firebase_user_document.dart
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FirebaseTimelineService
|
class FirebaseTimelineService extends TimelineService with TimelineUserService {
|
||||||
with ChangeNotifier
|
|
||||||
implements TimelineService, TimelineUserService {
|
|
||||||
FirebaseTimelineService({
|
FirebaseTimelineService({
|
||||||
required TimelineUserService userService,
|
required TimelineUserService userService,
|
||||||
FirebaseApp? app,
|
FirebaseApp? app,
|
||||||
|
@ -35,8 +33,6 @@ class FirebaseTimelineService
|
||||||
|
|
||||||
final Map<String, TimelinePosterUserModel> _users = {};
|
final Map<String, TimelinePosterUserModel> _users = {};
|
||||||
|
|
||||||
List<TimelinePost> _posts = [];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TimelinePost> createPost(TimelinePost post) async {
|
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||||
var postId = const Uuid().v4();
|
var postId = const Uuid().v4();
|
||||||
|
@ -52,14 +48,14 @@ class FirebaseTimelineService
|
||||||
var postRef =
|
var postRef =
|
||||||
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
_db.collection(_options.timelineCollectionName).doc(updatedPost.id);
|
||||||
await postRef.set(updatedPost.toJson());
|
await postRef.set(updatedPost.toJson());
|
||||||
_posts.add(updatedPost);
|
posts.add(updatedPost);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deletePost(TimelinePost post) async {
|
Future<void> deletePost(TimelinePost post) async {
|
||||||
_posts = _posts.where((element) => element.id != post.id).toList();
|
posts = posts.where((element) => element.id != post.id).toList();
|
||||||
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
await postRef.delete();
|
await postRef.delete();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -77,7 +73,7 @@ class FirebaseTimelineService
|
||||||
reaction: post.reaction - 1,
|
reaction: post.reaction - 1,
|
||||||
reactions: (post.reactions ?? [])..remove(reaction),
|
reactions: (post.reactions ?? [])..remove(reaction),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -107,7 +103,7 @@ class FirebaseTimelineService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var updatedPost = post.copyWith(reactions: updatedReactions);
|
var updatedPost = post.copyWith(reactions: updatedReactions);
|
||||||
_posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
@ -129,7 +125,7 @@ class FirebaseTimelineService
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||||
posts.add(post);
|
posts.add(post);
|
||||||
}
|
}
|
||||||
_posts = posts;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
@ -140,12 +136,12 @@ class FirebaseTimelineService
|
||||||
int limit,
|
int limit,
|
||||||
) async {
|
) async {
|
||||||
// only take posts that are in our category
|
// only take posts that are in our category
|
||||||
var oldestPost = _posts
|
var oldestPost = posts
|
||||||
.where(
|
.where(
|
||||||
(element) => category == null || element.category == category,
|
(element) => category == null || element.category == category,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
_posts.first,
|
posts.first,
|
||||||
(previousValue, element) =>
|
(previousValue, element) =>
|
||||||
(previousValue.createdAt.isBefore(element.createdAt))
|
(previousValue.createdAt.isBefore(element.createdAt))
|
||||||
? previousValue
|
? previousValue
|
||||||
|
@ -166,16 +162,16 @@ class FirebaseTimelineService
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.get();
|
.get();
|
||||||
// add the new posts to the list
|
// add the new posts to the list
|
||||||
var posts = <TimelinePost>[];
|
var newPosts = <TimelinePost>[];
|
||||||
for (var doc in snapshot.docs) {
|
for (var doc in snapshot.docs) {
|
||||||
var data = doc.data();
|
var data = doc.data();
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
var user = await _userService.getUser(data['creator_id']);
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||||
posts.add(post);
|
newPosts.add(post);
|
||||||
}
|
}
|
||||||
_posts = [..._posts, ...posts];
|
posts = [...posts, ...newPosts];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return newPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -190,7 +186,7 @@ class FirebaseTimelineService
|
||||||
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
|
var updatedPost = TimelinePost.fromJson(doc.id, data).copyWith(
|
||||||
creator: user,
|
creator: user,
|
||||||
);
|
);
|
||||||
_posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
posts = posts.map((p) => (p.id == post.id) ? updatedPost : p).toList();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return updatedPost;
|
return updatedPost;
|
||||||
}
|
}
|
||||||
|
@ -198,12 +194,12 @@ class FirebaseTimelineService
|
||||||
@override
|
@override
|
||||||
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
Future<List<TimelinePost>> refreshPosts(String? category) async {
|
||||||
// fetch all posts between now and the newest posts we have
|
// fetch all posts between now and the newest posts we have
|
||||||
var newestPostWeHave = _posts
|
var newestPostWeHave = posts
|
||||||
.where(
|
.where(
|
||||||
(element) => category == null || element.category == category,
|
(element) => category == null || element.category == category,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
_posts.first,
|
posts.first,
|
||||||
(previousValue, element) =>
|
(previousValue, element) =>
|
||||||
(previousValue.createdAt.isAfter(element.createdAt))
|
(previousValue.createdAt.isAfter(element.createdAt))
|
||||||
? previousValue
|
? previousValue
|
||||||
|
@ -220,26 +216,26 @@ class FirebaseTimelineService
|
||||||
.orderBy('created_at', descending: true)
|
.orderBy('created_at', descending: true)
|
||||||
.endBefore([newestPostWeHave.createdAt]).get();
|
.endBefore([newestPostWeHave.createdAt]).get();
|
||||||
// add the new posts to the list
|
// add the new posts to the list
|
||||||
var posts = <TimelinePost>[];
|
var newPosts = <TimelinePost>[];
|
||||||
for (var doc in snapshot.docs) {
|
for (var doc in snapshot.docs) {
|
||||||
var data = doc.data();
|
var data = doc.data();
|
||||||
var user = await _userService.getUser(data['creator_id']);
|
var user = await _userService.getUser(data['creator_id']);
|
||||||
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
|
||||||
posts.add(post);
|
newPosts.add(post);
|
||||||
}
|
}
|
||||||
_posts = [...posts, ..._posts];
|
posts = [...posts, ...newPosts];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return posts;
|
return newPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TimelinePost? getPost(String postId) =>
|
TimelinePost? getPost(String postId) =>
|
||||||
(_posts.any((element) => element.id == postId))
|
(posts.any((element) => element.id == postId))
|
||||||
? _posts.firstWhere((element) => element.id == postId)
|
? posts.firstWhere((element) => element.id == postId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<TimelinePost> getPosts(String? category) => _posts
|
List<TimelinePost> getPosts(String? category) => posts
|
||||||
.where((element) => category == null || element.category == category)
|
.where((element) => category == null || element.category == category)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
@ -250,7 +246,7 @@ class FirebaseTimelineService
|
||||||
likes: post.likes + 1,
|
likes: post.likes + 1,
|
||||||
likedBy: post.likedBy?..add(userId),
|
likedBy: post.likedBy?..add(userId),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -271,7 +267,7 @@ class FirebaseTimelineService
|
||||||
likes: post.likes - 1,
|
likes: post.likes - 1,
|
||||||
likedBy: post.likedBy?..remove(userId),
|
likedBy: post.likedBy?..remove(userId),
|
||||||
);
|
);
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
@ -314,7 +310,7 @@ class FirebaseTimelineService
|
||||||
'reaction': FieldValue.increment(1),
|
'reaction': FieldValue.increment(1),
|
||||||
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
'reactions': FieldValue.arrayUnion([updatedReaction.toJson()]),
|
||||||
});
|
});
|
||||||
_posts = _posts
|
posts = posts
|
||||||
.map(
|
.map(
|
||||||
(p) => p.id == post.id ? updatedPost : p,
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,5 +8,6 @@ export 'src/model/timeline_category.dart';
|
||||||
export 'src/model/timeline_post.dart';
|
export 'src/model/timeline_post.dart';
|
||||||
export 'src/model/timeline_poster.dart';
|
export 'src/model/timeline_poster.dart';
|
||||||
export 'src/model/timeline_reaction.dart';
|
export 'src/model/timeline_reaction.dart';
|
||||||
|
export 'src/services/filter_service.dart';
|
||||||
export 'src/services/timeline_service.dart';
|
export 'src/services/timeline_service.dart';
|
||||||
export 'src/services/user_service.dart';
|
export 'src/services/user_service.dart';
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||||
|
|
||||||
|
mixin TimelineFilterService on TimelineService {
|
||||||
|
List<TimelinePost> filterPosts(
|
||||||
|
String filterWord,
|
||||||
|
Map<String, dynamic> options,
|
||||||
|
) {
|
||||||
|
var filteredPosts = posts
|
||||||
|
.where(
|
||||||
|
(post) => post.title.toLowerCase().contains(
|
||||||
|
filterWord.toLowerCase(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return filteredPosts;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
|
||||||
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
||||||
|
|
||||||
abstract class TimelineService with ChangeNotifier {
|
abstract class TimelineService with ChangeNotifier {
|
||||||
|
List<TimelinePost> posts = [];
|
||||||
|
|
||||||
Future<void> deletePost(TimelinePost post);
|
Future<void> deletePost(TimelinePost post);
|
||||||
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
Future<TimelinePost> deletePostReaction(TimelinePost post, String reactionId);
|
||||||
Future<TimelinePost> createPost(TimelinePost post);
|
Future<TimelinePost> createPost(TimelinePost post);
|
||||||
|
|
|
@ -8,9 +8,8 @@ import 'package:flutter_timeline_view/src/config/timeline_theme.dart';
|
||||||
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@immutable
|
|
||||||
class TimelineOptions {
|
class TimelineOptions {
|
||||||
const TimelineOptions({
|
TimelineOptions({
|
||||||
this.theme = const TimelineTheme(),
|
this.theme = const TimelineTheme(),
|
||||||
this.translations = const TimelineTranslations.empty(),
|
this.translations = const TimelineTranslations.empty(),
|
||||||
this.imagePickerConfig = const ImagePickerConfig(),
|
this.imagePickerConfig = const ImagePickerConfig(),
|
||||||
|
@ -18,7 +17,7 @@ class TimelineOptions {
|
||||||
this.timelinePostHeight,
|
this.timelinePostHeight,
|
||||||
this.allowAllDeletion = false,
|
this.allowAllDeletion = false,
|
||||||
this.sortCommentsAscending = true,
|
this.sortCommentsAscending = true,
|
||||||
this.sortPostsAscending = false,
|
this.sortPostsAscending,
|
||||||
this.doubleTapTolike = false,
|
this.doubleTapTolike = false,
|
||||||
this.iconsWithValues = false,
|
this.iconsWithValues = false,
|
||||||
this.likeAndDislikeIconsForDoubleTap = const (
|
this.likeAndDislikeIconsForDoubleTap = const (
|
||||||
|
@ -44,6 +43,10 @@ class TimelineOptions {
|
||||||
this.categories,
|
this.categories,
|
||||||
this.categoryButtonBuilder,
|
this.categoryButtonBuilder,
|
||||||
this.catergoryLabelBuilder,
|
this.catergoryLabelBuilder,
|
||||||
|
this.categorySelectorHorizontalPadding,
|
||||||
|
this.filterEnabled = false,
|
||||||
|
this.initialFilterWord,
|
||||||
|
this.searchBarBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Theming options for the timeline
|
/// Theming options for the timeline
|
||||||
|
@ -59,7 +62,7 @@ class TimelineOptions {
|
||||||
final bool sortCommentsAscending;
|
final bool sortCommentsAscending;
|
||||||
|
|
||||||
/// Whether to sort posts ascending or descending
|
/// Whether to sort posts ascending or descending
|
||||||
final bool sortPostsAscending;
|
final bool? sortPostsAscending;
|
||||||
|
|
||||||
/// Allow all posts to be deleted instead of
|
/// Allow all posts to be deleted instead of
|
||||||
/// only the posts of the current user
|
/// only the posts of the current user
|
||||||
|
@ -132,6 +135,23 @@ class TimelineOptions {
|
||||||
/// Ability to set an proper label for the category selectors.
|
/// Ability to set an proper label for the category selectors.
|
||||||
/// Default to category key.
|
/// Default to category key.
|
||||||
final String Function(String? categoryKey)? catergoryLabelBuilder;
|
final String Function(String? categoryKey)? catergoryLabelBuilder;
|
||||||
|
|
||||||
|
/// Overides the standard horizontal padding of the whole category selector.
|
||||||
|
final double? categorySelectorHorizontalPadding;
|
||||||
|
|
||||||
|
/// if true the filter textfield is enabled.
|
||||||
|
bool filterEnabled;
|
||||||
|
|
||||||
|
/// Set a value to search through posts. When set the searchbar is shown.
|
||||||
|
/// If null no searchbar is shown.
|
||||||
|
final String? initialFilterWord;
|
||||||
|
|
||||||
|
final Widget Function(
|
||||||
|
Future<List<TimelinePost>> Function(
|
||||||
|
String filterWord,
|
||||||
|
Map<String, dynamic> options,
|
||||||
|
) search,
|
||||||
|
)? searchBarBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef ButtonBuilder = Widget Function(
|
typedef ButtonBuilder = Widget Function(
|
||||||
|
|
|
@ -28,6 +28,7 @@ class TimelineTranslations {
|
||||||
required this.postAt,
|
required this.postAt,
|
||||||
required this.postLoadingError,
|
required this.postLoadingError,
|
||||||
required this.timelineSelectionDescription,
|
required this.timelineSelectionDescription,
|
||||||
|
required this.searchHint,
|
||||||
});
|
});
|
||||||
|
|
||||||
const TimelineTranslations.empty()
|
const TimelineTranslations.empty()
|
||||||
|
@ -52,7 +53,8 @@ class TimelineTranslations {
|
||||||
writeComment = 'Write your comment here...',
|
writeComment = 'Write your comment here...',
|
||||||
postAt = 'at',
|
postAt = 'at',
|
||||||
postLoadingError = 'Something went wrong while loading the post',
|
postLoadingError = 'Something went wrong while loading the post',
|
||||||
timelineSelectionDescription = 'Choose a category';
|
timelineSelectionDescription = 'Choose a category',
|
||||||
|
searchHint = 'Search...';
|
||||||
|
|
||||||
final String noPosts;
|
final String noPosts;
|
||||||
final String noPostsWithFilter;
|
final String noPostsWithFilter;
|
||||||
|
@ -79,6 +81,8 @@ class TimelineTranslations {
|
||||||
|
|
||||||
final String timelineSelectionDescription;
|
final String timelineSelectionDescription;
|
||||||
|
|
||||||
|
final String searchHint;
|
||||||
|
|
||||||
TimelineTranslations copyWith({
|
TimelineTranslations copyWith({
|
||||||
String? noPosts,
|
String? noPosts,
|
||||||
String? noPostsWithFilter,
|
String? noPostsWithFilter,
|
||||||
|
@ -101,6 +105,7 @@ class TimelineTranslations {
|
||||||
String? firstComment,
|
String? firstComment,
|
||||||
String? postLoadingError,
|
String? postLoadingError,
|
||||||
String? timelineSelectionDescription,
|
String? timelineSelectionDescription,
|
||||||
|
String? searchHint,
|
||||||
}) =>
|
}) =>
|
||||||
TimelineTranslations(
|
TimelineTranslations(
|
||||||
noPosts: noPosts ?? this.noPosts,
|
noPosts: noPosts ?? this.noPosts,
|
||||||
|
@ -127,5 +132,6 @@ class TimelineTranslations {
|
||||||
postLoadingError: postLoadingError ?? this.postLoadingError,
|
postLoadingError: postLoadingError ?? this.postLoadingError,
|
||||||
timelineSelectionDescription:
|
timelineSelectionDescription:
|
||||||
timelineSelectionDescription ?? this.timelineSelectionDescription,
|
timelineSelectionDescription ?? this.timelineSelectionDescription,
|
||||||
|
searchHint: searchHint ?? this.searchHint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ class TimelineScreen extends StatefulWidget {
|
||||||
this.onPostTap,
|
this.onPostTap,
|
||||||
this.onUserTap,
|
this.onUserTap,
|
||||||
this.posts,
|
this.posts,
|
||||||
this.timelineCategoryFilter,
|
this.timelineCategory,
|
||||||
this.postWidget,
|
this.postWidget,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ class TimelineScreen extends StatefulWidget {
|
||||||
final ScrollController? scrollController;
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
/// The string to filter the timeline by category
|
/// The string to filter the timeline by category
|
||||||
final String? timelineCategoryFilter;
|
final String? timelineCategory;
|
||||||
|
|
||||||
/// This is used if you want to pass in a list of posts instead
|
/// This is used if you want to pass in a list of posts instead
|
||||||
/// of fetching them from the service
|
/// of fetching them from the service
|
||||||
|
@ -57,11 +57,15 @@ class TimelineScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _TimelineScreenState extends State<TimelineScreen> {
|
class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
late ScrollController controller;
|
late ScrollController controller;
|
||||||
|
late var textFieldController =
|
||||||
|
TextEditingController(text: widget.options.initialFilterWord);
|
||||||
late var service = widget.service;
|
late var service = widget.service;
|
||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
late var filter = widget.timelineCategoryFilter;
|
late var category = widget.timelineCategory;
|
||||||
|
|
||||||
|
late var filterWord = widget.options.initialFilterWord;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -80,32 +84,109 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: service,
|
listenable: service,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
var posts = widget.posts ?? service.getPosts(filter);
|
var posts = widget.posts ?? service.getPosts(category);
|
||||||
|
|
||||||
posts = posts
|
posts = posts
|
||||||
.where(
|
.where(
|
||||||
(p) => filter == null || p.category == filter,
|
(p) => category == null || p.category == category,
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
if (widget.options.filterEnabled && filterWord != null) {
|
||||||
|
if (service is TimelineFilterService?) {
|
||||||
|
posts =
|
||||||
|
(service as TimelineFilterService).filterPosts(filterWord!, {});
|
||||||
|
} else {
|
||||||
|
debugPrint('Timeline service needs to mixin'
|
||||||
|
' with TimelineFilterService');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sort posts by date
|
// sort posts by date
|
||||||
posts.sort(
|
if (widget.options.sortPostsAscending != null) {
|
||||||
(a, b) => widget.options.sortPostsAscending
|
posts.sort(
|
||||||
? a.createdAt.compareTo(b.createdAt)
|
(a, b) => widget.options.sortPostsAscending!
|
||||||
: b.createdAt.compareTo(a.createdAt),
|
? a.createdAt.compareTo(b.createdAt)
|
||||||
);
|
: b.createdAt.compareTo(a.createdAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: widget.options.padding.top,
|
||||||
|
),
|
||||||
|
if (widget.options.filterEnabled) ...[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: widget.options.padding.horizontal,
|
||||||
|
),
|
||||||
|
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();
|
||||||
|
widget.options.filterEnabled = false;
|
||||||
|
filterWord = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Color(0xFF000000),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
],
|
||||||
CategorySelector(
|
CategorySelector(
|
||||||
filter: filter,
|
filter: category,
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
onTapCategory: (categoryKey) {
|
onTapCategory: (categoryKey) {
|
||||||
setState(() {
|
setState(() {
|
||||||
filter = categoryKey;
|
category = categoryKey;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
@ -159,7 +240,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
filter == null
|
category == null
|
||||||
? widget.options.translations.noPosts
|
? widget.options.translations.noPosts
|
||||||
: widget.options.translations.noPostsWithFilter,
|
: widget.options.translations.noPostsWithFilter,
|
||||||
style: widget.options.theme.textStyles.noPostsStyle,
|
style: widget.options.theme.textStyles.noPostsStyle,
|
||||||
|
@ -170,6 +251,9 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: widget.options.padding.bottom,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -179,7 +263,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
||||||
Future<void> loadPosts() async {
|
Future<void> loadPosts() async {
|
||||||
if (widget.posts != null) return;
|
if (widget.posts != null) return;
|
||||||
try {
|
try {
|
||||||
await service.fetchPosts(filter);
|
await service.fetchPosts(category);
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||||
import 'package:flutter_timeline_view/src/widgets/category_selector_button.dart';
|
import 'package:flutter_timeline_view/src/widgets/category_selector_button.dart';
|
||||||
|
@ -22,49 +24,51 @@ class CategorySelector extends StatelessWidget {
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: EdgeInsets.symmetric(
|
children: [
|
||||||
horizontal: options.padding.horizontal,
|
SizedBox(
|
||||||
),
|
width: options.categorySelectorHorizontalPadding ??
|
||||||
child: Row(
|
max(options.padding.horizontal - 4, 0),
|
||||||
children: [
|
),
|
||||||
options.categoryButtonBuilder?.call(
|
options.categoryButtonBuilder?.call(
|
||||||
categoryKey: null,
|
categoryKey: null,
|
||||||
categoryName:
|
categoryName:
|
||||||
options.catergoryLabelBuilder?.call(null) ?? 'All',
|
options.catergoryLabelBuilder?.call(null) ?? 'All',
|
||||||
onTap: () => onTapCategory(null),
|
onTap: () => onTapCategory(null),
|
||||||
|
selected: filter == null,
|
||||||
|
) ??
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: CategorySelectorButton(
|
||||||
|
category: null,
|
||||||
selected: filter == null,
|
selected: filter == null,
|
||||||
|
onTap: () => onTapCategory(null),
|
||||||
|
labelBuilder: options.catergoryLabelBuilder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (var category in options.categories!) ...[
|
||||||
|
options.categoryButtonBuilder?.call(
|
||||||
|
categoryKey: category,
|
||||||
|
categoryName:
|
||||||
|
options.catergoryLabelBuilder?.call(category) ?? category,
|
||||||
|
onTap: () => onTapCategory(category),
|
||||||
|
selected: filter == category,
|
||||||
) ??
|
) ??
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: CategorySelectorButton(
|
child: CategorySelectorButton(
|
||||||
category: null,
|
category: category,
|
||||||
selected: filter == null,
|
selected: filter == category,
|
||||||
onTap: () => onTapCategory(null),
|
onTap: () => onTapCategory(category),
|
||||||
labelBuilder: options.catergoryLabelBuilder,
|
labelBuilder: options.catergoryLabelBuilder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (var category in options.categories!) ...[
|
|
||||||
options.categoryButtonBuilder?.call(
|
|
||||||
categoryKey: category,
|
|
||||||
categoryName:
|
|
||||||
options.catergoryLabelBuilder?.call(category) ??
|
|
||||||
category,
|
|
||||||
onTap: () => onTapCategory(category),
|
|
||||||
selected: filter == category,
|
|
||||||
) ??
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
child: CategorySelectorButton(
|
|
||||||
category: category,
|
|
||||||
selected: filter == category,
|
|
||||||
onTap: () => onTapCategory(category),
|
|
||||||
labelBuilder: options.catergoryLabelBuilder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
SizedBox(
|
||||||
|
width: options.categorySelectorHorizontalPadding ??
|
||||||
|
max(options.padding.horizontal - 4, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ class CategorySelectorButton extends StatelessWidget {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
padding: const MaterialStatePropertyAll(
|
padding: const MaterialStatePropertyAll(
|
||||||
EdgeInsets.symmetric(
|
EdgeInsets.symmetric(
|
||||||
vertical: 5,
|
vertical: 5,
|
||||||
|
|
|
@ -49,7 +49,9 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: widget.post.imageUrl != null ? widget.options.postWidgetheight : null,
|
height: widget.post.imageUrl != null
|
||||||
|
? widget.options.postWidgetheight
|
||||||
|
: null,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
Loading…
Reference in a new issue