feat: implement figma designs

This commit is contained in:
DirkIconica 2024-04-17 09:35:20 +02:00
parent 3ec780ea0a
commit f4990dbf5c
20 changed files with 365 additions and 144 deletions

View file

@ -25,9 +25,14 @@ class GoRouterApp extends StatelessWidget {
routerConfig: _router, routerConfig: _router,
title: 'Flutter Timeline', title: 'Flutter Timeline',
theme: ThemeData( theme: ThemeData(
colorScheme: textTheme: const TextTheme(
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith( titleLarge: TextStyle(
background: const Color(0xFFB8E2E8), color: Color(0xffb71c6d), fontFamily: 'Playfair Display')),
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFB8E2E8),
primary: const Color(0xffb71c6d),
).copyWith(
background: const Color(0XFFFAF9F6),
), ),
useMaterial3: true, useMaterial3: true,
), ),

View file

@ -34,7 +34,13 @@ class _PostScreenState extends State<PostScreen> {
class TestUserService implements TimelineUserService { class TestUserService implements TimelineUserService {
final Map<String, TimelinePosterUserModel> _users = { final Map<String, TimelinePosterUserModel> _users = {
'test_user': const TimelinePosterUserModel(userId: 'test_user') 'test_user': const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
)
}; };
@override @override

View file

@ -101,6 +101,13 @@ void generatePost(TimelineService service) {
content: "Post $amountOfPosts content", content: "Post $amountOfPosts content",
likes: 0, likes: 0,
reaction: 0, reaction: 0,
creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
createdAt: DateTime.now(), createdAt: DateTime.now(),
reactionEnabled: amountOfPosts % 2 == 0 ? false : true, reactionEnabled: amountOfPosts % 2 == 0 ? false : true,
imageUrl: amountOfPosts % 3 != 0 imageUrl: amountOfPosts % 3 != 0

View file

@ -1,6 +1,6 @@
// import 'package:example/apps/go_router/app.dart'; // import 'package:example/apps/go_router/app.dart';
// import 'package:example/apps/navigator/app.dart'; // import 'package:example/apps/navigator/app.dart';
import 'package:example/apps/widgets/app.dart'; import 'package:example/apps/go_router/app.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
@ -9,7 +9,7 @@ void main() {
// Uncomment any, but only one, of these lines to run the example with specific navigation. // Uncomment any, but only one, of these lines to run the example with specific navigation.
runApp(const WidgetApp()); // runApp(const WidgetApp());
// runApp(const NavigatorApp()); // runApp(const NavigatorApp());
// runApp(const GoRouterApp()); runApp(const GoRouterApp());
} }

View file

@ -0,0 +1,29 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:example/apps/widgets/app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const WidgetApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

View file

@ -43,10 +43,16 @@ List<GoRoute> getTimelineStoryRoutes({
); );
var button = FloatingActionButton( var button = FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor,
onPressed: () async => context.go( onPressed: () async => context.go(
TimelineUserStoryRoutes.timelinePostCreation, TimelineUserStoryRoutes.timelinePostCreation,
), ),
child: const Icon(Icons.add), shape: const CircleBorder(),
child: const Icon(
Icons.add,
color: Colors.white,
size: 30,
),
); );
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
@ -55,7 +61,13 @@ List<GoRoute> getTimelineStoryRoutes({
child: config.homeOpenPageBuilder child: config.homeOpenPageBuilder
?.call(context, timelineScreen, button) ?? ?.call(context, timelineScreen, button) ??
Scaffold( Scaffold(
appBar: AppBar(), appBar: AppBar(
backgroundColor: Colors.black,
title: Text(
'Iconinstagram',
style: Theme.of(context).textTheme.titleLarge,
),
),
body: timelineScreen, body: timelineScreen,
floatingActionButton: button, floatingActionButton: button,
), ),
@ -66,18 +78,19 @@ List<GoRoute> getTimelineStoryRoutes({
path: TimelineUserStoryRoutes.timelineView, path: TimelineUserStoryRoutes.timelineView,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var post = var post =
config.service.postService.getPost(state.pathParameters['post']!)!; config.service.postService.getPost(state.pathParameters['post']!);
var timelinePostWidget = TimelinePostScreen( var timelinePostWidget = TimelinePostScreen(
userId: config.userId, userId: config.userId,
options: config.optionsBuilder(context), options: config.optionsBuilder(context),
service: config.service, service: config.service,
post: post, post: post!,
onPostDelete: () => config.onPostDelete?.call(context, post), onPostDelete: () => config.onPostDelete?.call(context, post),
onUserTap: (user) => config.onUserTap?.call(context, user), onUserTap: (user) => config.onUserTap?.call(context, user),
); );
var backButton = IconButton( var backButton = IconButton(
color: Colors.white,
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome), onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome),
); );
@ -90,6 +103,11 @@ List<GoRoute> getTimelineStoryRoutes({
Scaffold( Scaffold(
appBar: AppBar( appBar: AppBar(
leading: backButton, leading: backButton,
backgroundColor: Colors.black,
title: Text(
'Category',
style: Theme.of(context).textTheme.titleLarge,
),
), ),
body: timelinePostWidget, body: timelinePostWidget,
), ),
@ -133,8 +151,10 @@ List<GoRoute> getTimelineStoryRoutes({
?.call(context, timelinePostCreationWidget, backButton) ?? ?.call(context, timelinePostCreationWidget, backButton) ??
Scaffold( Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.black,
title: Text( title: Text(
config.optionsBuilder(context).translations.postCreation, config.optionsBuilder(context).translations.postCreation,
style: Theme.of(context).textTheme.titleLarge,
), ),
leading: backButton, leading: backButton,
), ),

View file

@ -134,6 +134,7 @@ Widget _postCreationScreenRoute({
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
style: Theme.of(context).textTheme.titleLarge,
config.optionsBuilder(context).translations.postCreation, config.optionsBuilder(context).translations.postCreation,
), ),
), ),

View file

@ -8,7 +8,7 @@ version: 2.3.0
publish_to: none publish_to: none
environment: environment:
sdk: '>=3.1.3 <4.0.0' sdk: ">=3.1.3 <4.0.0"
dependencies: dependencies:
flutter: flutter:
@ -35,4 +35,3 @@ dev_dependencies:
ref: 6.0.0 ref: 6.0.0
flutter: flutter:

View file

@ -40,7 +40,8 @@ class TimelineOptions {
this.padding = const EdgeInsets.symmetric(vertical: 12.0), this.padding = const EdgeInsets.symmetric(vertical: 12.0),
this.iconSize = 26, this.iconSize = 26,
this.postWidgetHeight, this.postWidgetHeight,
this.postPadding = const EdgeInsets.all(12.0), this.postPadding =
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
this.filterOptions = const FilterOptions(), this.filterOptions = const FilterOptions(),
this.categoriesOptions = const CategoriesOptions(), this.categoriesOptions = const CategoriesOptions(),
this.requireImageForPost = false, this.requireImageForPost = false,

View file

@ -20,6 +20,7 @@ class TimelineTextStyles {
this.postTitleStyle, this.postTitleStyle,
this.postLikeTitleAndAmount, this.postLikeTitleAndAmount,
this.postCreatedAtStyle, this.postCreatedAtStyle,
this.categoryTitleStyle,
}); });
/// The TextStyle for the text indicating that you can view a post /// The TextStyle for the text indicating that you can view a post
@ -70,4 +71,6 @@ class TimelineTextStyles {
/// The TextStyle for the creation time of the post /// The TextStyle for the creation time of the post
final TextStyle? postCreatedAtStyle; final TextStyle? postCreatedAtStyle;
final TextStyle? categoryTitleStyle;
} }

View file

@ -11,12 +11,15 @@ class TimelineTranslations {
required this.noPosts, required this.noPosts,
required this.noPostsWithFilter, required this.noPostsWithFilter,
required this.title, required this.title,
required this.titleHintText,
required this.content, required this.content,
required this.contentHintText,
required this.contentDescription, required this.contentDescription,
required this.uploadImage, required this.uploadImage,
required this.uploadImageDescription, required this.uploadImageDescription,
required this.allowComments, required this.allowComments,
required this.allowCommentsDescription, required this.allowCommentsDescription,
required this.commentsTitleOnPost,
required this.checkPost, required this.checkPost,
required this.deletePost, required this.deletePost,
required this.deleteReaction, required this.deleteReaction,
@ -32,6 +35,8 @@ class TimelineTranslations {
required this.postOverview, required this.postOverview,
required this.postIn, required this.postIn,
required this.postCreation, required this.postCreation,
required this.yes,
required this.no,
}); });
const TimelineTranslations.empty() const TimelineTranslations.empty()
@ -46,12 +51,13 @@ class TimelineTranslations {
allowComments = 'Are people allowed to comment?', allowComments = 'Are people allowed to comment?',
allowCommentsDescription = allowCommentsDescription =
'Indicate whether people are allowed to respond', 'Indicate whether people are allowed to respond',
commentsTitleOnPost = 'Comments',
checkPost = 'Check post overview', checkPost = 'Check post overview',
deletePost = 'Delete post', deletePost = 'Delete post',
deleteReaction = 'Delete Reaction', deleteReaction = 'Delete Reaction',
viewPost = 'View post', viewPost = 'View post',
likesTitle = 'Likes', likesTitle = 'Likes',
commentsTitle = 'Comments', commentsTitle = 'Are people allowed to comment?',
firstComment = 'Be the first to comment', firstComment = 'Be the first to comment',
writeComment = 'Write your comment here...', writeComment = 'Write your comment here...',
postAt = 'at', postAt = 'at',
@ -60,7 +66,11 @@ class TimelineTranslations {
searchHint = 'Search...', searchHint = 'Search...',
postOverview = 'Post Overview', postOverview = 'Post Overview',
postIn = 'Post in', postIn = 'Post in',
postCreation = 'Create Post'; postCreation = 'Create Post',
titleHintText = 'Title...',
contentHintText = 'Context...',
yes = 'Yes',
no = 'No';
final String noPosts; final String noPosts;
final String noPostsWithFilter; final String noPostsWithFilter;
@ -76,11 +86,15 @@ class TimelineTranslations {
final String checkPost; final String checkPost;
final String postAt; final String postAt;
final String titleHintText;
final String contentHintText;
final String deletePost; final String deletePost;
final String deleteReaction; final String deleteReaction;
final String viewPost; final String viewPost;
final String likesTitle; final String likesTitle;
final String commentsTitle; final String commentsTitle;
final String commentsTitleOnPost;
final String writeComment; final String writeComment;
final String firstComment; final String firstComment;
final String postLoadingError; final String postLoadingError;
@ -93,6 +107,9 @@ class TimelineTranslations {
final String postIn; final String postIn;
final String postCreation; final String postCreation;
final String yes;
final String no;
TimelineTranslations copyWith({ TimelineTranslations copyWith({
String? noPosts, String? noPosts,
String? noPostsWithFilter, String? noPostsWithFilter,
@ -104,6 +121,7 @@ class TimelineTranslations {
String? uploadImageDescription, String? uploadImageDescription,
String? allowComments, String? allowComments,
String? allowCommentsDescription, String? allowCommentsDescription,
String? commentsTitleOnPost,
String? checkPost, String? checkPost,
String? postAt, String? postAt,
String? deletePost, String? deletePost,
@ -119,6 +137,10 @@ class TimelineTranslations {
String? postOverview, String? postOverview,
String? postIn, String? postIn,
String? postCreation, String? postCreation,
String? titleHintText,
String? contentHintText,
String? yes,
String? no,
}) => }) =>
TimelineTranslations( TimelineTranslations(
noPosts: noPosts ?? this.noPosts, noPosts: noPosts ?? this.noPosts,
@ -133,6 +155,7 @@ class TimelineTranslations {
allowComments: allowComments ?? this.allowComments, allowComments: allowComments ?? this.allowComments,
allowCommentsDescription: allowCommentsDescription:
allowCommentsDescription ?? this.allowCommentsDescription, allowCommentsDescription ?? this.allowCommentsDescription,
commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost,
checkPost: checkPost ?? this.checkPost, checkPost: checkPost ?? this.checkPost,
postAt: postAt ?? this.postAt, postAt: postAt ?? this.postAt,
deletePost: deletePost ?? this.deletePost, deletePost: deletePost ?? this.deletePost,
@ -149,5 +172,9 @@ class TimelineTranslations {
postOverview: postOverview ?? this.postOverview, postOverview: postOverview ?? this.postOverview,
postIn: postIn ?? this.postIn, postIn: postIn ?? this.postIn,
postCreation: postCreation ?? this.postCreation, postCreation: postCreation ?? this.postCreation,
titleHintText: titleHintText ?? this.titleHintText,
contentHintText: contentHintText ?? this.contentHintText,
yes: yes ?? this.yes,
no: no ?? this.no,
); );
} }

View file

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dotted_border/dotted_border.dart'; import 'package:dotted_border/dotted_border.dart';
@ -97,7 +98,7 @@ class _TimelinePostCreationScreenState
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future<void> onPostCreated() async { Future<void> onPostCreated() async {
var post = TimelinePost( var post = TimelinePost(
id: '', id: 'Post${Random().nextInt(1000)}',
creatorId: widget.userId, creatorId: widget.userId,
title: titleController.text, title: titleController.text,
category: widget.postCategory, category: widget.postCategory,
@ -128,7 +129,7 @@ class _TimelinePostCreationScreenState
children: [ children: [
Text( Text(
widget.options.translations.title, widget.options.translations.title,
style: theme.textTheme.displaySmall, style: theme.textTheme.titleMedium,
), ),
widget.options.textInputBuilder?.call( widget.options.textInputBuilder?.call(
titleController, titleController,
@ -137,11 +138,14 @@ class _TimelinePostCreationScreenState
) ?? ) ??
TextField( TextField(
controller: titleController, controller: titleController,
decoration: InputDecoration(
hintText: widget.options.translations.titleHintText,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
widget.options.translations.content, widget.options.translations.content,
style: theme.textTheme.displaySmall, style: theme.textTheme.titleMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
@ -157,6 +161,9 @@ class _TimelinePostCreationScreenState
expands: true, expands: true,
maxLines: null, maxLines: null,
minLines: null, minLines: null,
decoration: InputDecoration(
hintText: widget.options.translations.contentHintText,
),
), ),
), ),
const SizedBox( const SizedBox(
@ -165,7 +172,7 @@ class _TimelinePostCreationScreenState
// input field for the content // input field for the content
Text( Text(
widget.options.translations.uploadImage, widget.options.translations.uploadImage,
style: theme.textTheme.displaySmall, style: theme.textTheme.titleMedium,
), ),
Text( Text(
widget.options.translations.uploadImageDescription, widget.options.translations.uploadImageDescription,
@ -198,18 +205,18 @@ class _TimelinePostCreationScreenState
} }
checkIfEditingDone(); checkIfEditingDone();
}, },
child: image != null child: ClipRRect(
? ClipRRect(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
child: Image.memory( child: image != null
? Image.memory(
image!, image!,
width: double.infinity, width: double.infinity,
height: 150.0, height: 150.0,
fit: BoxFit.cover, fit: BoxFit.cover,
// give it a rounded border // give it a rounded border
),
) )
: DottedBorder( : DottedBorder(
dashPattern: const [4, 4],
radius: const Radius.circular(8.0), radius: const Radius.circular(8.0),
color: theme.textTheme.displayMedium?.color ?? color: theme.textTheme.displayMedium?.color ??
Colors.white, Colors.white,
@ -218,7 +225,8 @@ class _TimelinePostCreationScreenState
height: 150.0, height: 150.0,
child: Icon( child: Icon(
Icons.image, Icons.image,
size: 32, size: 50,
),
), ),
), ),
), ),
@ -255,21 +263,36 @@ class _TimelinePostCreationScreenState
Text( Text(
widget.options.translations.commentsTitle, widget.options.translations.commentsTitle,
style: theme.textTheme.displaySmall, style: theme.textTheme.titleMedium,
), ),
Text( Text(
widget.options.translations.allowCommentsDescription, widget.options.translations.allowCommentsDescription,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
// radio buttons for yes or no Row(
Switch( mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Checkbox(
value: allowComments, value: allowComments,
onChanged: (newValue) { onChanged: (value) {
setState(() { setState(() {
allowComments = newValue; allowComments = true;
}); });
}, },
), ),
Text(widget.options.translations.yes),
Checkbox(
value: !allowComments,
onChanged: (value) {
setState(() {
allowComments = false;
});
},
),
Text(widget.options.translations.no),
],
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: (widget.options.buttonBuilder != null) child: (widget.options.buttonBuilder != null)

View file

@ -21,7 +21,11 @@ class TimelinePostOverviewScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(options.translations.postOverview), backgroundColor: Colors.black,
title: Text(
options.translations.postOverview,
style: TextStyle(color: Theme.of(context).primaryColor),
),
), ),
body: Column( body: Column(
children: [ children: [

View file

@ -93,6 +93,10 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText, hintText: hintText,
suffixIcon: suffixIcon, suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(20.0), // Adjust the value as needed
),
), ),
); );
@ -184,7 +188,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
if (post.creator!.imageUrl != null) ...[ if (post.creator!.imageUrl != null) ...[
widget.options.userAvatarBuilder?.call( widget.options.userAvatarBuilder?.call(
post.creator!, post.creator!,
40, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 20, radius: 20,
@ -196,7 +200,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
] else ...[ ] else ...[
widget.options.anonymousAvatarBuilder?.call( widget.options.anonymousAvatarBuilder?.call(
post.creator!, post.creator!,
40, 28,
) ?? ) ??
const CircleAvatar( const CircleAvatar(
radius: 20, radius: 20,
@ -318,6 +322,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
Icon( Icon(
Icons.thumb_up_rounded, Icons.thumb_up_rounded,
color: widget.options.theme.iconColor, color: widget.options.theme.iconColor,
size: widget.options.iconSize,
), ),
), ),
), ),
@ -418,8 +423,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
const SizedBox(height: 20), const SizedBox(height: 20),
if (post.reactionEnabled) ...[ if (post.reactionEnabled) ...[
Text( Text(
widget.options.translations.commentsTitle, widget.options.translations.commentsTitleOnPost,
style: theme.textTheme.displaySmall, style: theme.textTheme.titleMedium,
), ),
for (var reaction for (var reaction
in post.reactions ?? <TimelinePostReaction>[]) ...[ in post.reactions ?? <TimelinePostReaction>[]) ...[
@ -469,7 +474,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
reaction.creator!.imageUrl!.isNotEmpty) ...[ reaction.creator!.imageUrl!.isNotEmpty) ...[
widget.options.userAvatarBuilder?.call( widget.options.userAvatarBuilder?.call(
reaction.creator!, reaction.creator!,
25, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 20, radius: 20,
@ -480,7 +485,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
] else ...[ ] else ...[
widget.options.anonymousAvatarBuilder?.call( widget.options.anonymousAvatarBuilder?.call(
reaction.creator!, reaction.creator!,
25, 28,
) ?? ) ??
const CircleAvatar( const CircleAvatar(
radius: 20, radius: 20,

View file

@ -74,10 +74,27 @@ class _TimelineScreenState extends State<TimelineScreen> {
late var filterWord = widget.options.filterOptions.initialFilterWord; late var filterWord = widget.options.filterOptions.initialFilterWord;
bool _isOnTop = true;
@override
void dispose() {
controller.removeListener(_updateIsOnTop);
controller.dispose();
super.dispose();
}
void _updateIsOnTop() {
setState(() {
_isOnTop = controller.position.pixels < 40;
});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
controller = widget.scrollController ?? ScrollController(); controller = widget.scrollController ?? ScrollController();
controller.addListener(_updateIsOnTop);
// only load the posts after the first frame // only load the posts after the first frame
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(loadPosts()); unawaited(loadPosts());
@ -188,6 +205,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
), ),
], ],
CategorySelector( CategorySelector(
isOnTop: _isOnTop,
filter: category, filter: category,
options: widget.options, options: widget.options,
onTapCategory: (categoryKey) { onTapCategory: (categoryKey) {

View file

@ -17,7 +17,13 @@ class LocalTimelinePostService
Future<TimelinePost> createPost(TimelinePost post) async { Future<TimelinePost> createPost(TimelinePost post) async {
posts.add( posts.add(
post.copyWith( post.copyWith(
creator: const TimelinePosterUserModel(userId: 'test_user'), creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
), ),
); );
notifyListeners(); notifyListeners();
@ -62,7 +68,13 @@ class LocalTimelinePostService
for (var reaction in reactions) { for (var reaction in reactions) {
updatedReactions.add( updatedReactions.add(
reaction.copyWith( reaction.copyWith(
creator: const TimelinePosterUserModel(userId: 'test_user'), creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
), ),
); );
} }
@ -156,7 +168,13 @@ class LocalTimelinePostService
var updatedReaction = reaction.copyWith( var updatedReaction = reaction.copyWith(
id: reactionId, id: reactionId,
creator: const TimelinePosterUserModel(userId: 'test_user'), creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
); );
var updatedPost = post.copyWith( var updatedPost = post.copyWith(
@ -179,11 +197,20 @@ class LocalTimelinePostService
creatorId: 'test_user', creatorId: 'test_user',
title: 'Post 0', title: 'Post 0',
category: null, category: null,
imageUrl:
'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg',
content: 'Standard post without image made by the current user', content: 'Standard post without image made by the current user',
likes: 0, likes: 0,
reaction: 0, reaction: 0,
createdAt: DateTime.now(), createdAt: DateTime.now(),
reactionEnabled: false, reactionEnabled: false,
creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
), ),
TimelinePost( TimelinePost(
id: 'Post1', id: 'Post1',
@ -197,7 +224,14 @@ class LocalTimelinePostService
createdAt: DateTime.now(), createdAt: DateTime.now(),
reactionEnabled: false, reactionEnabled: false,
imageUrl: imageUrl:
'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2', 'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg',
creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
), ),
TimelinePost( TimelinePost(
id: 'Post2', id: 'Post2',
@ -211,7 +245,14 @@ class LocalTimelinePostService
createdAt: DateTime.now(), createdAt: DateTime.now(),
reactionEnabled: true, reactionEnabled: true,
imageUrl: imageUrl:
'https://s3-eu-west-1.amazonaws.com/sortlist-core-api/6qpvvqjtmniirpkvp8eg83bicnc2', 'https://t4.ftcdn.net/jpg/02/77/71/45/240_F_277714513_fQ0akmI3TQxa0wkPCLeO12Rx3cL2AuIf.jpg',
creator: const TimelinePosterUserModel(
userId: 'test_user',
imageUrl:
'https://cdn.britannica.com/68/143568-050-5246474F/Donkey.jpg?w=400&h=300&c=crop',
firstName: 'Dirk',
lastName: 'lukassen',
),
), ),
]; ];
} }

View file

@ -3,55 +3,64 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline_view/flutter_timeline_view.dart'; import 'package:flutter_timeline_view/flutter_timeline_view.dart';
class CategorySelector extends StatelessWidget { class CategorySelector extends StatefulWidget {
const CategorySelector({ const CategorySelector({
required this.filter, required this.filter,
required this.options, required this.options,
required this.onTapCategory, required this.onTapCategory,
required this.isOnTop,
super.key, super.key,
}); });
final String? filter; final String? filter;
final TimelineOptions options; final TimelineOptions options;
final void Function(String? categoryKey) onTapCategory; final void Function(String? categoryKey) onTapCategory;
final bool isOnTop;
@override
State<CategorySelector> createState() => _CategorySelectorState();
}
class _CategorySelectorState extends State<CategorySelector> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (options.categoriesOptions.categoriesBuilder == null) { if (widget.options.categoriesOptions.categoriesBuilder == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
var categories = options.categoriesOptions.categoriesBuilder!(context); var categories =
widget.options.categoriesOptions.categoriesBuilder!(context);
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
width: width: widget.options.categoriesOptions
options.categoriesOptions.categorySelectorHorizontalPadding ?? .categorySelectorHorizontalPadding ??
max(options.padding.horizontal - 4, 0), max(widget.options.padding.horizontal - 20, 0),
), ),
for (var category in categories) ...[ for (var category in categories) ...[
options.categoriesOptions.categoryButtonBuilder?.call( widget.options.categoriesOptions.categoryButtonBuilder?.call(
categoryKey: category.key, categoryKey: category.key,
categoryName: category.title, categoryName: category.title,
onTap: () => onTapCategory(category.key), onTap: () => widget.onTapCategory(category.key),
selected: filter == category.key, selected: widget.filter == category.key,
) ?? ) ??
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: CategorySelectorButton( child: CategorySelectorButton(
isOnTop: widget.isOnTop,
category: category, category: category,
selected: filter == category.key, selected: widget.filter == category.key,
onTap: () => onTapCategory(category.key), onTap: () => widget.onTapCategory(category.key),
options: widget.options,
), ),
), ),
], ],
SizedBox( SizedBox(
width: width: widget.options.categoriesOptions
options.categoriesOptions.categorySelectorHorizontalPadding ?? .categorySelectorHorizontalPadding ??
max(options.padding.horizontal - 4, 0), max(widget.options.padding.horizontal - 4, 0),
), ),
], ],
), ),

View file

@ -1,23 +1,31 @@
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/flutter_timeline_view.dart';
class CategorySelectorButton extends StatelessWidget { class CategorySelectorButton extends StatelessWidget {
const CategorySelectorButton({ const CategorySelectorButton({
required this.category, required this.category,
required this.selected, required this.selected,
required this.onTap, required this.onTap,
required this.options,
required this.isOnTop,
super.key, super.key,
}); });
final TimelineCategory category; final TimelineCategory category;
final bool selected; final bool selected;
final void Function() onTap; final void Function() onTap;
final TimelineOptions options;
final bool isOnTop;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return TextButton( return AnimatedContainer(
height: isOnTop ? 140 : 40,
duration: const Duration(milliseconds: 100),
child: TextButton(
onPressed: onTap, onPressed: onTap,
style: ButtonStyle( style: ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
@ -27,26 +35,44 @@ class CategorySelectorButton extends StatelessWidget {
horizontal: 12, horizontal: 12,
), ),
), ),
minimumSize: const MaterialStatePropertyAll(Size.zero), fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
backgroundColor: MaterialStatePropertyAll( backgroundColor: MaterialStatePropertyAll(
selected ? theme.colorScheme.primary : theme.colorScheme.surface, selected ? theme.colorScheme.primary : Colors.transparent,
), ),
shape: const MaterialStatePropertyAll( shape: MaterialStatePropertyAll(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(45), Radius.circular(8),
),
side: BorderSide(
color: theme.colorScheme.primary,
width: 2,
), ),
), ),
), ),
), ),
child: Text( child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
mainAxisAlignment:
isOnTop ? MainAxisAlignment.end : MainAxisAlignment.center,
children: [
Text(
category.title, category.title,
style: theme.textTheme.labelMedium?.copyWith( style: (options.theme.textStyles.categoryTitleStyle ??
theme.textTheme.labelLarge)
?.copyWith(
color: selected color: selected
? theme.colorScheme.onPrimary ? theme.colorScheme.onPrimary
: theme.colorScheme.onSurface, : theme.colorScheme.onSurface,
), ),
), ),
],
),
],
),
),
); );
} }
} }

View file

@ -30,32 +30,28 @@ class _ReactionBottomState extends State<ReactionBottom> {
final TextEditingController _textEditingController = TextEditingController(); final TextEditingController _textEditingController = TextEditingController();
@override @override
Widget build(BuildContext context) => Container( Widget build(BuildContext context) => SafeArea(
bottom: true,
child: Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Container( child: Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
), ),
height: 45, height: 48,
child: widget.messageInputBuilder( child: widget.messageInputBuilder(
_textEditingController, _textEditingController,
Padding( Padding(
padding: const EdgeInsets.only(right: 15.0), padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton(
onPressed: widget.onPressSelectImage,
icon: Icon(
Icons.image,
color: widget.iconColor,
),
),
IconButton( IconButton(
onPressed: () async { onPressed: () async {
var value = _textEditingController.text; var value = _textEditingController.text;
if (value.isNotEmpty) { if (value.isNotEmpty) {
await widget.onReactionSubmit(value); await widget.onReactionSubmit(value);
_textEditingController.clear(); _textEditingController.clear();
@ -72,5 +68,6 @@ class _ReactionBottomState extends State<ReactionBottom> {
widget.translations.writeComment, widget.translations.writeComment,
), ),
), ),
),
); );
} }

View file

@ -69,7 +69,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
if (widget.post.creator!.imageUrl != null) ...[ if (widget.post.creator!.imageUrl != null) ...[
widget.options.userAvatarBuilder?.call( widget.options.userAvatarBuilder?.call(
widget.post.creator!, widget.post.creator!,
40, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 20, radius: 20,
@ -213,7 +213,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
icon: widget.options.theme.likeIcon ?? icon: widget.options.theme.likeIcon ??
Icon( Icon(
widget.post.likedBy?.contains(widget.userId) ?? false widget.post.likedBy?.contains(widget.userId) ?? false
? Icons.favorite ? Icons.favorite_rounded
: Icons.favorite_outline_outlined, : Icons.favorite_outline_outlined,
), ),
label: Text('${widget.post.likes}'), label: Text('${widget.post.likes}'),
@ -240,7 +240,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
color: Colors.transparent, color: Colors.transparent,
child: widget.options.theme.likedIcon ?? child: widget.options.theme.likedIcon ??
Icon( Icon(
Icons.thumb_up_rounded, Icons.favorite_rounded,
color: widget.options.theme.iconColor, color: widget.options.theme.iconColor,
size: widget.options.iconSize, size: widget.options.iconSize,
), ),
@ -253,7 +253,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
color: Colors.transparent, color: Colors.transparent,
child: widget.options.theme.likedIcon ?? child: widget.options.theme.likedIcon ??
Icon( Icon(
Icons.thumb_up_rounded, Icons.favorite_outline,
color: widget.options.theme.iconColor, color: widget.options.theme.iconColor,
size: widget.options.iconSize, size: widget.options.iconSize,
), ),