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'; import 'package:flutter/material.dart';
void main() { void main() {

View file

@ -5,7 +5,9 @@
library flutter_timeline_view; library flutter_timeline_view;
export 'src/config/timeline_options.dart'; export 'src/config/timeline_options.dart';
export 'src/config/timeline_theme.dart';
export 'src/config/timeline_translations.dart'; export 'src/config/timeline_translations.dart';
export 'src/screens/timeline_post_creation_screen.dart'; export 'src/screens/timeline_post_creation_screen.dart';
export 'src/screens/timeline_post_screen.dart'; export 'src/screens/timeline_post_screen.dart';
export 'src/screens/timeline_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.imagePickerConfig = const ImagePickerConfig(),
this.imagePickerTheme = const ImagePickerTheme(), this.imagePickerTheme = const ImagePickerTheme(),
this.sortCommentsAscending = false, this.sortCommentsAscending = false,
this.sortPostsAscending = false,
this.dateformat, this.dateformat,
this.timeFormat, this.timeFormat,
this.buttonBuilder, this.buttonBuilder,
@ -35,6 +36,9 @@ class TimelineOptions {
/// Whether to sort comments ascending or descending /// Whether to sort comments ascending or descending
final bool sortCommentsAscending; final bool sortCommentsAscending;
/// Whether to sort posts ascending or descending
final bool sortPostsAscending;
final TimelineTranslations translations; final TimelineTranslations translations;
final ButtonBuilder? buttonBuilder; final ButtonBuilder? buttonBuilder;

View file

@ -12,6 +12,8 @@ class TimelineTheme {
this.commentIcon, this.commentIcon,
this.likedIcon, this.likedIcon,
this.sendIcon, this.sendIcon,
this.moreIcon,
this.deleteIcon,
}); });
final Color? iconColor; final Color? iconColor;
@ -27,4 +29,10 @@ class TimelineTheme {
/// The icon to display to submit a comment /// The icon to display to submit a comment
final Widget? sendIcon; 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.userService,
required this.options, required this.options,
required this.post, required this.post,
this.onUserTap,
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16), this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
super.key, super.key,
}); });
@ -42,6 +43,9 @@ class TimelinePostScreen extends StatefulWidget {
/// The padding around the screen /// The padding around the screen
final EdgeInsets padding; final EdgeInsets padding;
/// If this is not null, the user can tap on the user avatar or name
final Function(String userId)? onUserTap;
@override @override
State<TimelinePostScreen> createState() => _TimelinePostScreenState(); State<TimelinePostScreen> createState() => _TimelinePostScreenState();
} }
@ -110,34 +114,44 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (post.creator != null) Row(
Row( children: [
children: [ if (post.creator != null)
if (post.creator!.imageUrl != null) ...[ InkWell(
widget.options.userAvatarBuilder?.call( onTap: widget.onUserTap != null
post.creator!, ? () => widget.onUserTap?.call(post.creator!.userId)
40, : null,
) ?? child: Row(
CircleAvatar( children: [
radius: 20, if (post.creator!.imageUrl != null) ...[
backgroundImage: CachedNetworkImageProvider( widget.options.userAvatarBuilder?.call(
post.creator!.imageUrl!, 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,
), ),
], ),
const Spacer(),
// three small dots at the end widget.options.theme.moreIcon ??
const Spacer(), const Icon(
const Icon(Icons.more_horiz), Icons.more_horiz_rounded,
], ),
), ],
),
const SizedBox(height: 8), const SizedBox(height: 8),
// image of the post // image of the post
if (post.imageUrl != null) ...[ if (post.imageUrl != null) ...[

View file

@ -13,32 +13,47 @@ class TimelineScreen extends StatefulWidget {
const TimelineScreen({ const TimelineScreen({
required this.userId, required this.userId,
required this.options, required this.options,
required this.posts,
required this.onPostTap, required this.onPostTap,
required this.service, required this.service,
this.onUserTap,
this.posts,
this.controller, this.controller,
this.timelineCategoryFilter, this.timelineCategoryFilter,
this.timelinePostHeight = 100.0, this.timelinePostHeight = 100.0,
this.padding = const EdgeInsets.symmetric(vertical: 12.0),
super.key, super.key,
}); });
/// The user id of the current user /// The user id of the current user
final String userId; final String userId;
/// The service to use for fetching and manipulating posts
final TimelineService service; final TimelineService service;
/// All the configuration options for the timelinescreens and widgets
final TimelineOptions options; final TimelineOptions options;
/// The controller for the scroll view
final ScrollController? controller; final ScrollController? controller;
final String? timelineCategoryFilter; final String? timelineCategoryFilter;
/// The height of a post in the timeline
final double timelinePostHeight; 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; 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 @override
State<TimelineScreen> createState() => _TimelineScreenState(); State<TimelineScreen> createState() => _TimelineScreenState();
} }
@ -55,7 +70,71 @@ class _TimelineScreenState extends State<TimelineScreen> {
unawaited(loadPosts()); 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 { Future<void> loadPosts() async {
if (widget.posts != null) return;
try { try {
// Fetching posts from the service // Fetching posts from the service
var fetchedPosts = 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: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:flutter_timeline_interface/flutter_timeline_interface.dart';
@ -12,6 +16,7 @@ class TimelinePostWidget extends StatelessWidget {
required this.onTapLike, required this.onTapLike,
required this.onTapUnlike, required this.onTapUnlike,
required this.onTap, required this.onTap,
this.onUserTap,
super.key, super.key,
}); });
@ -25,6 +30,9 @@ class TimelinePostWidget extends StatelessWidget {
final VoidCallback onTapLike; final VoidCallback onTapLike;
final VoidCallback onTapUnlike; final VoidCallback onTapUnlike;
/// If this is not null, the user can tap on the user avatar or name
final Function(String userId)? onUserTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
@ -36,34 +44,44 @@ class TimelinePostWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (post.creator != null) Row(
Row( children: [
children: [ if (post.creator != null)
if (post.creator!.imageUrl != null) ...[ InkWell(
options.userAvatarBuilder?.call( onTap: onUserTap != null
post.creator!, ? () => onUserTap?.call(post.creator!.userId)
40, : null,
) ?? child: Row(
CircleAvatar( children: [
radius: 20, if (post.creator!.imageUrl != null) ...[
backgroundImage: CachedNetworkImageProvider( options.userAvatarBuilder?.call(
post.creator!.imageUrl!, 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,
), ),
], ),
const Spacer(),
// three small dots at the end options.theme.moreIcon ??
const Spacer(), const Icon(
const Icon(Icons.more_horiz), Icons.more_horiz_rounded,
], ),
), ],
),
const SizedBox(height: 8), const SizedBox(height: 8),
// image of the post // image of the post
if (post.imageUrl != null) ...[ if (post.imageUrl != null) ...[