feat: add timeline post detail screen

This commit is contained in:
Freek van de Ven 2023-11-19 23:11:53 +01:00
parent c33d8cb893
commit fb8ca56a87
10 changed files with 285 additions and 78 deletions

View file

@ -3,6 +3,7 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
@immutable @immutable
class TimelinePostReaction { class TimelinePostReaction {
@ -12,6 +13,7 @@ class TimelinePostReaction {
required this.creatorId, required this.creatorId,
required this.reaction, required this.reaction,
required this.createdAt, required this.createdAt,
this.creator,
}); });
/// The unique identifier of the reaction. /// The unique identifier of the reaction.
@ -23,6 +25,9 @@ class TimelinePostReaction {
/// The unique identifier of the creator of the reaction. /// The unique identifier of the creator of the reaction.
final String creatorId; final String creatorId;
/// The creator of the post. If null it isn't loaded yet.
final TimelinePosterUserModel? creator;
/// The reactiontext /// The reactiontext
final String reaction; final String reaction;

View file

@ -4,24 +4,33 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart'; import 'package:flutter_image_picker/flutter_image_picker.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
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 @immutable
class TimelineOptions { class TimelineOptions {
const TimelineOptions({ const TimelineOptions({
this.theme = const TimelineTheme(),
this.translations = const TimelineTranslations(), this.translations = const TimelineTranslations(),
this.imagePickerConfig = const ImagePickerConfig(), this.imagePickerConfig = const ImagePickerConfig(),
this.imagePickerTheme = const ImagePickerTheme(), this.imagePickerTheme = const ImagePickerTheme(),
this.dateformat, this.dateformat,
this.timeFormat,
this.buttonBuilder, this.buttonBuilder,
this.textInputBuilder, this.textInputBuilder,
this.userAvatarBuilder, this.userAvatarBuilder,
}); });
/// The format to display the post time in /// Theming options for the timeline
final TimelineTheme theme;
/// The format to display the post date in
final DateFormat? dateformat; final DateFormat? dateformat;
/// The format to display the post time in
final DateFormat? timeFormat;
final TimelineTranslations translations; final TimelineTranslations translations;
final ButtonBuilder? buttonBuilder; final ButtonBuilder? buttonBuilder;

View file

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
@immutable
class TimelineTheme {
const TimelineTheme({
this.iconColor,
});
final Color? iconColor;
}

View file

@ -21,6 +21,7 @@ class TimelineTranslations {
this.likesTitle = 'Likes', this.likesTitle = 'Likes',
this.commentsTitle = 'Comments', this.commentsTitle = 'Comments',
this.writeComment = 'Write your comment here...', this.writeComment = 'Write your comment here...',
this.postAt = 'at',
}); });
final String title; final String title;
@ -31,6 +32,7 @@ class TimelineTranslations {
final String allowComments; final String allowComments;
final String allowCommentsDescription; final String allowCommentsDescription;
final String checkPost; final String checkPost;
final String postAt;
final String deletePost; final String deletePost;
final String viewPost; final String viewPost;

View file

@ -4,10 +4,10 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart'; import 'package:flutter_image_picker/flutter_image_picker.dart';
import 'package:flutter_timeline_view/src/config/timeline_options.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart';
import 'package:flutter_timeline_view/src/widgets/dotted_container.dart';
class TimelinePostCreationScreen extends StatefulWidget { class TimelinePostCreationScreen extends StatefulWidget {
const TimelinePostCreationScreen({ const TimelinePostCreationScreen({
@ -147,14 +147,10 @@ class _TimelinePostCreationScreenState
// give it a rounded border // give it a rounded border
), ),
) )
: CustomPaint( : DottedBorder(
painter: DashedBorderPainter( radius: const Radius.circular(8.0),
color: theme.textTheme.displayMedium?.color ?? color: theme.textTheme.displayMedium?.color ??
Colors.white, Colors.white,
dashLength: 4.0,
dashWidth: 1.5,
space: 4,
),
child: const SizedBox( child: const SizedBox(
width: double.infinity, width: double.infinity,
height: 150.0, height: 150.0,

View file

@ -2,17 +2,186 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
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';
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart';
import 'package:intl/intl.dart';
class TimelinePostScreen extends StatelessWidget { class TimelinePostScreen extends StatelessWidget {
const TimelinePostScreen({ const TimelinePostScreen({
required this.options,
required this.post, required this.post,
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
super.key, super.key,
}); });
final TimelineOptions options;
final TimelinePost post; final TimelinePost post;
/// The padding around the screen
final EdgeInsets padding;
@override @override
Widget build(BuildContext context) => const Placeholder(); Widget build(BuildContext context) {
var theme = Theme.of(context);
var dateFormat = options.dateformat ??
DateFormat('dd/MM/yyyy', Localizations.localeOf(context).languageCode);
var timeFormat = options.timeFormat ?? DateFormat('HH:mm');
return Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: padding,
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!,
),
),
],
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 SizedBox(height: 8),
// image of the post
if (post.imageUrl != null) ...[
CachedNetworkImage(
imageUrl: post.imageUrl!,
width: double.infinity,
fit: BoxFit.fitHeight,
),
],
// post information
Row(
children: [
// like icon
IconButton(
onPressed: () {},
icon: const Icon(Icons.thumb_up_rounded),
),
// comment icon
IconButton(
onPressed: () {},
icon: const Icon(
Icons.chat_bubble_outline_rounded,
),
),
],
),
Text(
'${post.likes} ${options.translations.likesTitle}',
style: theme.textTheme.titleSmall,
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
post.creator?.fullName ?? '',
style: theme.textTheme.titleSmall,
),
const SizedBox(width: 8),
Text(
post.title,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.fade,
),
],
),
Text(
post.content,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'${dateFormat.format(post.createdAt)} '
'${options.translations.postAt} '
'${timeFormat.format(post.createdAt)}',
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 12),
Text(
options.translations.commentsTitle,
style: theme.textTheme.displaySmall,
),
for (var reaction
in post.reactions ?? <TimelinePostReaction>[]) ...[
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (reaction.creator?.imageUrl != null &&
reaction.creator!.imageUrl!.isNotEmpty) ...[
options.userAvatarBuilder?.call(
reaction.creator!,
25,
) ??
CircleAvatar(
radius: 20,
backgroundImage: CachedNetworkImageProvider(
reaction.creator!.imageUrl!,
),
),
],
const SizedBox(width: 10),
if (reaction.creator?.fullName != null) ...[
Text(
reaction.creator!.fullName!,
style: theme.textTheme.titleSmall,
),
],
const SizedBox(width: 10),
Expanded(
child: Text(
reaction.reaction,
style: theme.textTheme.bodyMedium,
// text should go to new line
softWrap: true,
),
),
],
),
const SizedBox(height: 100),
],
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: ReactionBottom(
messageInputBuilder: options.textInputBuilder!,
onPressSelectImage: () async {},
onReactionSubmit: (reaction) async {},
translations: options.translations,
iconColor: options.theme.iconColor,
),
),
],
);
}
} }

View file

@ -1,65 +0,0 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
class DashedBorderPainter extends CustomPainter {
DashedBorderPainter({
this.color = Colors.black,
this.dashWidth = 2.0,
this.dashLength = 6.0,
this.space = 3.0,
});
final Color color;
final double dashWidth;
final double dashLength;
final double space;
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = color
..strokeWidth = dashWidth;
var x = 0.0;
var y = 0.0;
// Top border
while (x < size.width) {
canvas.drawLine(Offset(x, 0), Offset(x + dashLength, 0), paint);
x += dashLength + space;
}
// Right border
while (y < size.height) {
canvas.drawLine(
Offset(size.width, y),
Offset(size.width, y + dashLength),
paint,
);
y += dashLength + space;
}
x = size.width;
// Bottom border
while (x > 0) {
canvas.drawLine(
Offset(x, size.height),
Offset(x - dashLength, size.height),
paint,
);
x -= dashLength + space;
}
y = size.height;
// Left border
while (y > 0) {
canvas.drawLine(Offset(0, y), Offset(0, y - dashLength), paint);
y -= dashLength + space;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

View file

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.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.onPressSelectImage,
this.iconColor,
super.key,
});
final Future<void> Function(String text) onReactionSubmit;
final TextInputBuilder messageInputBuilder;
final VoidCallback? onPressSelectImage;
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(
color: Theme.of(context).colorScheme.background,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
height: 45,
child: widget.messageInputBuilder(
_textEditingController,
Padding(
padding: const EdgeInsets.only(right: 15.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: widget.onPressSelectImage,
icon: Icon(
Icons.image,
color: widget.iconColor,
),
),
IconButton(
onPressed: () async {
var value = _textEditingController.text;
if (value.isNotEmpty) {
await widget.onReactionSubmit(value);
_textEditingController.clear();
}
},
icon: Icon(
Icons.send,
color: widget.iconColor,
),
),
],
),
),
widget.translations.writeComment,
),
),
);
}

View file

@ -98,7 +98,7 @@ class TimelinePostWidget extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
post.content, post.title,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
), ),

View file

@ -16,7 +16,8 @@ dependencies:
sdk: flutter sdk: flutter
intl: any intl: any
cached_network_image: ^3.2.2 cached_network_image: ^3.2.2
dotted_border: ^2.1.0
flutter_timeline_interface: flutter_timeline_interface:
git: git:
url: https://github.com/Iconica-Development/flutter_timeline.git url: https://github.com/Iconica-Development/flutter_timeline.git