mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 18:43:45 +02:00
feat: small improvements to the screens
This commit is contained in:
parent
8792079fa4
commit
ca4ec03002
7 changed files with 183 additions and 89 deletions
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2023 Iconica
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
library flutter_timeline_view;
|
||||
|
||||
export 'src/config/timeline_options.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_screen.dart';
|
||||
export 'src/screens/timeline_screen.dart';
|
||||
export 'src/widgets/timeline_post_widget.dart';
|
||||
|
|
|
@ -16,6 +16,7 @@ class TimelineOptions {
|
|||
this.imagePickerConfig = const ImagePickerConfig(),
|
||||
this.imagePickerTheme = const ImagePickerTheme(),
|
||||
this.sortCommentsAscending = false,
|
||||
this.sortPostsAscending = false,
|
||||
this.dateformat,
|
||||
this.timeFormat,
|
||||
this.buttonBuilder,
|
||||
|
@ -35,6 +36,9 @@ class TimelineOptions {
|
|||
/// Whether to sort comments ascending or descending
|
||||
final bool sortCommentsAscending;
|
||||
|
||||
/// Whether to sort posts ascending or descending
|
||||
final bool sortPostsAscending;
|
||||
|
||||
final TimelineTranslations translations;
|
||||
|
||||
final ButtonBuilder? buttonBuilder;
|
||||
|
|
|
@ -12,6 +12,8 @@ class TimelineTheme {
|
|||
this.commentIcon,
|
||||
this.likedIcon,
|
||||
this.sendIcon,
|
||||
this.moreIcon,
|
||||
this.deleteIcon,
|
||||
});
|
||||
|
||||
final Color? iconColor;
|
||||
|
@ -27,4 +29,10 @@ class TimelineTheme {
|
|||
|
||||
/// 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;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ class TimelinePostScreen extends StatefulWidget {
|
|||
required this.userService,
|
||||
required this.options,
|
||||
required this.post,
|
||||
this.onUserTap,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
super.key,
|
||||
});
|
||||
|
@ -42,6 +43,9 @@ class TimelinePostScreen extends StatefulWidget {
|
|||
/// The padding around the screen
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// If this is not null, the user can tap on the user avatar or name
|
||||
final Function(String userId)? onUserTap;
|
||||
|
||||
@override
|
||||
State<TimelinePostScreen> createState() => _TimelinePostScreenState();
|
||||
}
|
||||
|
@ -110,34 +114,44 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (post.creator != null)
|
||||
Row(
|
||||
children: [
|
||||
if (post.creator!.imageUrl != null) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
40,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
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!,
|
||||
40,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (post.creator!.fullName != null) ...[
|
||||
Text(
|
||||
post.creator!.fullName!,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (post.creator!.fullName != null) ...[
|
||||
Text(
|
||||
post.creator!.fullName!,
|
||||
style: theme.textTheme.titleMedium,
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// three small dots at the end
|
||||
const Spacer(),
|
||||
const Icon(Icons.more_horiz),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
widget.options.theme.moreIcon ??
|
||||
const Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// image of the post
|
||||
if (post.imageUrl != null) ...[
|
||||
|
|
|
@ -13,32 +13,47 @@ class TimelineScreen extends StatefulWidget {
|
|||
const TimelineScreen({
|
||||
required this.userId,
|
||||
required this.options,
|
||||
required this.posts,
|
||||
required this.onPostTap,
|
||||
required this.service,
|
||||
this.onUserTap,
|
||||
this.posts,
|
||||
this.controller,
|
||||
this.timelineCategoryFilter,
|
||||
this.timelinePostHeight = 100.0,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 12.0),
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The user id of the current user
|
||||
final String userId;
|
||||
|
||||
/// 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? controller;
|
||||
|
||||
final String? timelineCategoryFilter;
|
||||
|
||||
/// The height of a post in the timeline
|
||||
final double timelinePostHeight;
|
||||
|
||||
final List<TimelinePost> posts;
|
||||
/// 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;
|
||||
|
||||
/// If this is not null, the user can tap on the user avatar or name
|
||||
final Function(String userId)? onUserTap;
|
||||
|
||||
/// The padding between posts in the timeline
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
State<TimelineScreen> createState() => _TimelineScreenState();
|
||||
}
|
||||
|
@ -55,7 +70,71 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
unawaited(loadPosts());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading && widget.posts == null) {
|
||||
// Show loading indicator while data is being fetched
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
var posts = widget.posts ?? this.posts;
|
||||
posts = posts
|
||||
.where(
|
||||
(p) =>
|
||||
widget.timelineCategoryFilter == null ||
|
||||
p.category == widget.timelineCategoryFilter,
|
||||
)
|
||||
.toList();
|
||||
|
||||
// sort posts by date
|
||||
posts.sort(
|
||||
(a, b) => widget.options.sortPostsAscending
|
||||
? b.createdAt.compareTo(a.createdAt)
|
||||
: a.createdAt.compareTo(b.createdAt),
|
||||
);
|
||||
|
||||
// Build the list of posts
|
||||
return SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
...posts.map(
|
||||
(post) => Padding(
|
||||
padding: widget.padding,
|
||||
child: TimelinePostWidget(
|
||||
userId: widget.userId,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
height: widget.timelinePostHeight,
|
||||
onTap: () => widget.onPostTap.call(post),
|
||||
onTapLike: () async => updatePostInList(
|
||||
await widget.service.likePost(widget.userId, post),
|
||||
),
|
||||
onTapUnlike: () async => updatePostInList(
|
||||
await widget.service.unlikePost(widget.userId, post),
|
||||
),
|
||||
onUserTap: widget.onUserTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (posts.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
widget.timelineCategoryFilter == null
|
||||
? widget.options.translations.noPosts
|
||||
: widget.options.translations.noPostsWithFilter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadPosts() async {
|
||||
if (widget.posts != null) return;
|
||||
try {
|
||||
// Fetching posts from the service
|
||||
var fetchedPosts =
|
||||
|
@ -86,39 +165,4 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading) {
|
||||
// Show loading indicator while data is being fetched
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Build the list of posts
|
||||
return SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: posts
|
||||
.map(
|
||||
(post) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: TimelinePostWidget(
|
||||
userId: widget.userId,
|
||||
options: widget.options,
|
||||
post: post,
|
||||
height: widget.timelinePostHeight,
|
||||
onTap: () => widget.onPostTap.call(post),
|
||||
onTapLike: () async => updatePostInList(
|
||||
await widget.service.likePost(widget.userId, post),
|
||||
),
|
||||
onTapUnlike: () async => updatePostInList(
|
||||
await widget.service.unlikePost(widget.userId, post),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// 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_timeline_interface/flutter_timeline_interface.dart';
|
||||
|
@ -12,6 +16,7 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
required this.onTapLike,
|
||||
required this.onTapUnlike,
|
||||
required this.onTap,
|
||||
this.onUserTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -25,6 +30,9 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
final VoidCallback onTapLike;
|
||||
final VoidCallback onTapUnlike;
|
||||
|
||||
/// If this is not null, the user can tap on the user avatar or name
|
||||
final Function(String userId)? onUserTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
@ -36,34 +44,44 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (post.creator != null)
|
||||
Row(
|
||||
children: [
|
||||
if (post.creator!.imageUrl != null) ...[
|
||||
options.userAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
40,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
Row(
|
||||
children: [
|
||||
if (post.creator != null)
|
||||
InkWell(
|
||||
onTap: onUserTap != null
|
||||
? () => onUserTap?.call(post.creator!.userId)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
if (post.creator!.imageUrl != null) ...[
|
||||
options.userAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
40,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
post.creator!.imageUrl!,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (post.creator!.fullName != null) ...[
|
||||
Text(
|
||||
post.creator!.fullName!,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (post.creator!.fullName != null) ...[
|
||||
Text(
|
||||
post.creator!.fullName!,
|
||||
style: theme.textTheme.titleMedium,
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// three small dots at the end
|
||||
const Spacer(),
|
||||
const Icon(Icons.more_horiz),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
options.theme.moreIcon ??
|
||||
const Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// image of the post
|
||||
if (post.imageUrl != null) ...[
|
||||
|
|
Loading…
Reference in a new issue