feat: small improvements to the screens

This commit is contained in:
Freek van de Ven 2023-11-21 17:56:52 +01:00
parent 8792079fa4
commit ca4ec03002
7 changed files with 183 additions and 89 deletions

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
void main() {

View file

@ -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';

View file

@ -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;

View file

@ -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;
}

View file

@ -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) ...[

View file

@ -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(),
),
);
}
}

View file

@ -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) ...[