mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
feat: add timeline post detail screen
This commit is contained in:
parent
c33d8cb893
commit
fb8ca56a87
10 changed files with 285 additions and 78 deletions
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
|
||||
|
||||
@immutable
|
||||
class TimelinePostReaction {
|
||||
|
@ -12,6 +13,7 @@ class TimelinePostReaction {
|
|||
required this.creatorId,
|
||||
required this.reaction,
|
||||
required this.createdAt,
|
||||
this.creator,
|
||||
});
|
||||
|
||||
/// The unique identifier of the reaction.
|
||||
|
@ -23,6 +25,9 @@ class TimelinePostReaction {
|
|||
/// The unique identifier of the creator of the reaction.
|
||||
final String creatorId;
|
||||
|
||||
/// The creator of the post. If null it isn't loaded yet.
|
||||
final TimelinePosterUserModel? creator;
|
||||
|
||||
/// The reactiontext
|
||||
final String reaction;
|
||||
|
||||
|
|
|
@ -4,24 +4,33 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_image_picker/flutter_image_picker.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:intl/intl.dart';
|
||||
|
||||
@immutable
|
||||
class TimelineOptions {
|
||||
const TimelineOptions({
|
||||
this.theme = const TimelineTheme(),
|
||||
this.translations = const TimelineTranslations(),
|
||||
this.imagePickerConfig = const ImagePickerConfig(),
|
||||
this.imagePickerTheme = const ImagePickerTheme(),
|
||||
this.dateformat,
|
||||
this.timeFormat,
|
||||
this.buttonBuilder,
|
||||
this.textInputBuilder,
|
||||
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;
|
||||
|
||||
/// The format to display the post time in
|
||||
final DateFormat? timeFormat;
|
||||
|
||||
final TimelineTranslations translations;
|
||||
|
||||
final ButtonBuilder? buttonBuilder;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -21,6 +21,7 @@ class TimelineTranslations {
|
|||
this.likesTitle = 'Likes',
|
||||
this.commentsTitle = 'Comments',
|
||||
this.writeComment = 'Write your comment here...',
|
||||
this.postAt = 'at',
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
@ -31,6 +32,7 @@ class TimelineTranslations {
|
|||
final String allowComments;
|
||||
final String allowCommentsDescription;
|
||||
final String checkPost;
|
||||
final String postAt;
|
||||
|
||||
final String deletePost;
|
||||
final String viewPost;
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:flutter/material.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/widgets/dotted_container.dart';
|
||||
|
||||
class TimelinePostCreationScreen extends StatefulWidget {
|
||||
const TimelinePostCreationScreen({
|
||||
|
@ -147,14 +147,10 @@ class _TimelinePostCreationScreenState
|
|||
// give it a rounded border
|
||||
),
|
||||
)
|
||||
: CustomPaint(
|
||||
painter: DashedBorderPainter(
|
||||
color: theme.textTheme.displayMedium?.color ??
|
||||
Colors.white,
|
||||
dashLength: 4.0,
|
||||
dashWidth: 1.5,
|
||||
space: 4,
|
||||
),
|
||||
: DottedBorder(
|
||||
radius: const Radius.circular(8.0),
|
||||
color: theme.textTheme.displayMedium?.color ??
|
||||
Colors.white,
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
|
|
|
@ -2,17 +2,186 @@
|
|||
//
|
||||
// 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';
|
||||
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 {
|
||||
const TimelinePostScreen({
|
||||
required this.options,
|
||||
required this.post,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TimelineOptions options;
|
||||
|
||||
final TimelinePost post;
|
||||
|
||||
/// The padding around the screen
|
||||
final EdgeInsets padding;
|
||||
|
||||
@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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -98,7 +98,7 @@ class TimelinePostWidget extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
post.content,
|
||||
post.title,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
|
|
|
@ -16,6 +16,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
intl: any
|
||||
cached_network_image: ^3.2.2
|
||||
dotted_border: ^2.1.0
|
||||
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
|
|
Loading…
Reference in a new issue