mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 02:23:46 +02:00
feat: implement figma designs
This commit is contained in:
parent
3ec780ea0a
commit
f4990dbf5c
20 changed files with 365 additions and 144 deletions
|
@ -25,9 +25,14 @@ class GoRouterApp extends StatelessWidget {
|
|||
routerConfig: _router,
|
||||
title: 'Flutter Timeline',
|
||||
theme: ThemeData(
|
||||
colorScheme:
|
||||
ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
|
||||
background: const Color(0xFFB8E2E8),
|
||||
textTheme: const TextTheme(
|
||||
titleLarge: TextStyle(
|
||||
color: Color(0xffb71c6d), fontFamily: 'Playfair Display')),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFB8E2E8),
|
||||
primary: const Color(0xffb71c6d),
|
||||
).copyWith(
|
||||
background: const Color(0XFFFAF9F6),
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
|
|
|
@ -34,7 +34,13 @@ class _PostScreenState extends State<PostScreen> {
|
|||
|
||||
class TestUserService implements TimelineUserService {
|
||||
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
|
||||
|
|
|
@ -101,6 +101,13 @@ void generatePost(TimelineService service) {
|
|||
content: "Post $amountOfPosts content",
|
||||
likes: 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(),
|
||||
reactionEnabled: amountOfPosts % 2 == 0 ? false : true,
|
||||
imageUrl: amountOfPosts % 3 != 0
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// import 'package:example/apps/go_router/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: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.
|
||||
|
||||
runApp(const WidgetApp());
|
||||
// runApp(const WidgetApp());
|
||||
// runApp(const NavigatorApp());
|
||||
// runApp(const GoRouterApp());
|
||||
runApp(const GoRouterApp());
|
||||
}
|
||||
|
|
29
packages/flutter_timeline/example/test/widget_test.dart
Normal file
29
packages/flutter_timeline/example/test/widget_test.dart
Normal 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);
|
||||
});
|
||||
}
|
|
@ -43,10 +43,16 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
);
|
||||
|
||||
var button = FloatingActionButton(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
onPressed: () async => context.go(
|
||||
TimelineUserStoryRoutes.timelinePostCreation,
|
||||
),
|
||||
child: const Icon(Icons.add),
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
);
|
||||
|
||||
return buildScreenWithoutTransition(
|
||||
|
@ -55,7 +61,13 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
child: config.homeOpenPageBuilder
|
||||
?.call(context, timelineScreen, button) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: Text(
|
||||
'Iconinstagram',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
body: timelineScreen,
|
||||
floatingActionButton: button,
|
||||
),
|
||||
|
@ -66,18 +78,19 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
path: TimelineUserStoryRoutes.timelineView,
|
||||
pageBuilder: (context, state) {
|
||||
var post =
|
||||
config.service.postService.getPost(state.pathParameters['post']!)!;
|
||||
config.service.postService.getPost(state.pathParameters['post']!);
|
||||
|
||||
var timelinePostWidget = TimelinePostScreen(
|
||||
userId: config.userId,
|
||||
options: config.optionsBuilder(context),
|
||||
service: config.service,
|
||||
post: post,
|
||||
post: post!,
|
||||
onPostDelete: () => config.onPostDelete?.call(context, post),
|
||||
onUserTap: (user) => config.onUserTap?.call(context, user),
|
||||
);
|
||||
|
||||
var backButton = IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
onPressed: () => context.go(TimelineUserStoryRoutes.timelineHome),
|
||||
);
|
||||
|
@ -90,6 +103,11 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: backButton,
|
||||
backgroundColor: Colors.black,
|
||||
title: Text(
|
||||
'Category',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
body: timelinePostWidget,
|
||||
),
|
||||
|
@ -133,8 +151,10 @@ List<GoRoute> getTimelineStoryRoutes({
|
|||
?.call(context, timelinePostCreationWidget, backButton) ??
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: Text(
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
leading: backButton,
|
||||
),
|
||||
|
|
|
@ -134,6 +134,7 @@ Widget _postCreationScreenRoute({
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
config.optionsBuilder(context).translations.postCreation,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -8,13 +8,13 @@ version: 2.3.0
|
|||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.3 <4.0.0'
|
||||
sdk: ">=3.1.3 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
go_router: any
|
||||
|
||||
|
||||
flutter_timeline_view:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
|
@ -22,10 +22,10 @@ dependencies:
|
|||
ref: 2.3.0
|
||||
|
||||
flutter_timeline_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 2.3.0
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_timeline
|
||||
path: packages/flutter_timeline_interface
|
||||
ref: 2.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
|
@ -35,4 +35,3 @@ dev_dependencies:
|
|||
ref: 6.0.0
|
||||
|
||||
flutter:
|
||||
|
||||
|
|
|
@ -40,7 +40,8 @@ class TimelineOptions {
|
|||
this.padding = const EdgeInsets.symmetric(vertical: 12.0),
|
||||
this.iconSize = 26,
|
||||
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.categoriesOptions = const CategoriesOptions(),
|
||||
this.requireImageForPost = false,
|
||||
|
|
|
@ -20,6 +20,7 @@ class TimelineTextStyles {
|
|||
this.postTitleStyle,
|
||||
this.postLikeTitleAndAmount,
|
||||
this.postCreatedAtStyle,
|
||||
this.categoryTitleStyle,
|
||||
});
|
||||
|
||||
/// 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
|
||||
final TextStyle? postCreatedAtStyle;
|
||||
|
||||
final TextStyle? categoryTitleStyle;
|
||||
}
|
||||
|
|
|
@ -11,12 +11,15 @@ class TimelineTranslations {
|
|||
required this.noPosts,
|
||||
required this.noPostsWithFilter,
|
||||
required this.title,
|
||||
required this.titleHintText,
|
||||
required this.content,
|
||||
required this.contentHintText,
|
||||
required this.contentDescription,
|
||||
required this.uploadImage,
|
||||
required this.uploadImageDescription,
|
||||
required this.allowComments,
|
||||
required this.allowCommentsDescription,
|
||||
required this.commentsTitleOnPost,
|
||||
required this.checkPost,
|
||||
required this.deletePost,
|
||||
required this.deleteReaction,
|
||||
|
@ -32,6 +35,8 @@ class TimelineTranslations {
|
|||
required this.postOverview,
|
||||
required this.postIn,
|
||||
required this.postCreation,
|
||||
required this.yes,
|
||||
required this.no,
|
||||
});
|
||||
|
||||
const TimelineTranslations.empty()
|
||||
|
@ -46,12 +51,13 @@ class TimelineTranslations {
|
|||
allowComments = 'Are people allowed to comment?',
|
||||
allowCommentsDescription =
|
||||
'Indicate whether people are allowed to respond',
|
||||
commentsTitleOnPost = 'Comments',
|
||||
checkPost = 'Check post overview',
|
||||
deletePost = 'Delete post',
|
||||
deleteReaction = 'Delete Reaction',
|
||||
viewPost = 'View post',
|
||||
likesTitle = 'Likes',
|
||||
commentsTitle = 'Comments',
|
||||
commentsTitle = 'Are people allowed to comment?',
|
||||
firstComment = 'Be the first to comment',
|
||||
writeComment = 'Write your comment here...',
|
||||
postAt = 'at',
|
||||
|
@ -60,7 +66,11 @@ class TimelineTranslations {
|
|||
searchHint = 'Search...',
|
||||
postOverview = 'Post Overview',
|
||||
postIn = 'Post in',
|
||||
postCreation = 'Create Post';
|
||||
postCreation = 'Create Post',
|
||||
titleHintText = 'Title...',
|
||||
contentHintText = 'Context...',
|
||||
yes = 'Yes',
|
||||
no = 'No';
|
||||
|
||||
final String noPosts;
|
||||
final String noPostsWithFilter;
|
||||
|
@ -76,11 +86,15 @@ class TimelineTranslations {
|
|||
final String checkPost;
|
||||
final String postAt;
|
||||
|
||||
final String titleHintText;
|
||||
final String contentHintText;
|
||||
|
||||
final String deletePost;
|
||||
final String deleteReaction;
|
||||
final String viewPost;
|
||||
final String likesTitle;
|
||||
final String commentsTitle;
|
||||
final String commentsTitleOnPost;
|
||||
final String writeComment;
|
||||
final String firstComment;
|
||||
final String postLoadingError;
|
||||
|
@ -93,6 +107,9 @@ class TimelineTranslations {
|
|||
final String postIn;
|
||||
final String postCreation;
|
||||
|
||||
final String yes;
|
||||
final String no;
|
||||
|
||||
TimelineTranslations copyWith({
|
||||
String? noPosts,
|
||||
String? noPostsWithFilter,
|
||||
|
@ -104,6 +121,7 @@ class TimelineTranslations {
|
|||
String? uploadImageDescription,
|
||||
String? allowComments,
|
||||
String? allowCommentsDescription,
|
||||
String? commentsTitleOnPost,
|
||||
String? checkPost,
|
||||
String? postAt,
|
||||
String? deletePost,
|
||||
|
@ -119,6 +137,10 @@ class TimelineTranslations {
|
|||
String? postOverview,
|
||||
String? postIn,
|
||||
String? postCreation,
|
||||
String? titleHintText,
|
||||
String? contentHintText,
|
||||
String? yes,
|
||||
String? no,
|
||||
}) =>
|
||||
TimelineTranslations(
|
||||
noPosts: noPosts ?? this.noPosts,
|
||||
|
@ -133,6 +155,7 @@ class TimelineTranslations {
|
|||
allowComments: allowComments ?? this.allowComments,
|
||||
allowCommentsDescription:
|
||||
allowCommentsDescription ?? this.allowCommentsDescription,
|
||||
commentsTitleOnPost: commentsTitleOnPost ?? this.commentsTitleOnPost,
|
||||
checkPost: checkPost ?? this.checkPost,
|
||||
postAt: postAt ?? this.postAt,
|
||||
deletePost: deletePost ?? this.deletePost,
|
||||
|
@ -149,5 +172,9 @@ class TimelineTranslations {
|
|||
postOverview: postOverview ?? this.postOverview,
|
||||
postIn: postIn ?? this.postIn,
|
||||
postCreation: postCreation ?? this.postCreation,
|
||||
titleHintText: titleHintText ?? this.titleHintText,
|
||||
contentHintText: contentHintText ?? this.contentHintText,
|
||||
yes: yes ?? this.yes,
|
||||
no: no ?? this.no,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
|
@ -97,7 +98,7 @@ class _TimelinePostCreationScreenState
|
|||
Widget build(BuildContext context) {
|
||||
Future<void> onPostCreated() async {
|
||||
var post = TimelinePost(
|
||||
id: '',
|
||||
id: 'Post${Random().nextInt(1000)}',
|
||||
creatorId: widget.userId,
|
||||
title: titleController.text,
|
||||
category: widget.postCategory,
|
||||
|
@ -128,7 +129,7 @@ class _TimelinePostCreationScreenState
|
|||
children: [
|
||||
Text(
|
||||
widget.options.translations.title,
|
||||
style: theme.textTheme.displaySmall,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
widget.options.textInputBuilder?.call(
|
||||
titleController,
|
||||
|
@ -137,11 +138,14 @@ class _TimelinePostCreationScreenState
|
|||
) ??
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.options.translations.titleHintText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.options.translations.content,
|
||||
style: theme.textTheme.displaySmall,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
|
@ -157,6 +161,9 @@ class _TimelinePostCreationScreenState
|
|||
expands: true,
|
||||
maxLines: null,
|
||||
minLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.options.translations.contentHintText,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -165,7 +172,7 @@ class _TimelinePostCreationScreenState
|
|||
// input field for the content
|
||||
Text(
|
||||
widget.options.translations.uploadImage,
|
||||
style: theme.textTheme.displaySmall,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.uploadImageDescription,
|
||||
|
@ -198,30 +205,31 @@ class _TimelinePostCreationScreenState
|
|||
}
|
||||
checkIfEditingDone();
|
||||
},
|
||||
child: image != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Image.memory(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: image != null
|
||||
? Image.memory(
|
||||
image!,
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
fit: BoxFit.cover,
|
||||
// give it a rounded border
|
||||
),
|
||||
)
|
||||
: DottedBorder(
|
||||
radius: const Radius.circular(8.0),
|
||||
color: theme.textTheme.displayMedium?.color ??
|
||||
Colors.white,
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
size: 32,
|
||||
)
|
||||
: DottedBorder(
|
||||
dashPattern: const [4, 4],
|
||||
radius: const Radius.circular(8.0),
|
||||
color: theme.textTheme.displayMedium?.color ??
|
||||
Colors.white,
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
height: 150.0,
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// if an image is selected, show a delete button
|
||||
if (image != null) ...[
|
||||
|
@ -255,21 +263,36 @@ class _TimelinePostCreationScreenState
|
|||
|
||||
Text(
|
||||
widget.options.translations.commentsTitle,
|
||||
style: theme.textTheme.displaySmall,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.options.translations.allowCommentsDescription,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// radio buttons for yes or no
|
||||
Switch(
|
||||
value: allowComments,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
allowComments = newValue;
|
||||
});
|
||||
},
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
allowComments = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(widget.options.translations.yes),
|
||||
Checkbox(
|
||||
value: !allowComments,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
allowComments = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(widget.options.translations.no),
|
||||
],
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: (widget.options.buttonBuilder != null)
|
||||
|
|
|
@ -21,7 +21,11 @@ class TimelinePostOverviewScreen extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(options.translations.postOverview),
|
||||
backgroundColor: Colors.black,
|
||||
title: Text(
|
||||
options.translations.postOverview,
|
||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
|
|
|
@ -93,6 +93,10 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
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) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
40,
|
||||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
|
@ -196,7 +200,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
] else ...[
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
post.creator!,
|
||||
40,
|
||||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 20,
|
||||
|
@ -318,6 +322,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
Icon(
|
||||
Icons.thumb_up_rounded,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -418,8 +423,8 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
const SizedBox(height: 20),
|
||||
if (post.reactionEnabled) ...[
|
||||
Text(
|
||||
widget.options.translations.commentsTitle,
|
||||
style: theme.textTheme.displaySmall,
|
||||
widget.options.translations.commentsTitleOnPost,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
for (var reaction
|
||||
in post.reactions ?? <TimelinePostReaction>[]) ...[
|
||||
|
@ -469,7 +474,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
reaction.creator!.imageUrl!.isNotEmpty) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
reaction.creator!,
|
||||
25,
|
||||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
|
@ -480,7 +485,7 @@ class _TimelinePostScreenState extends State<_TimelinePostScreen> {
|
|||
] else ...[
|
||||
widget.options.anonymousAvatarBuilder?.call(
|
||||
reaction.creator!,
|
||||
25,
|
||||
28,
|
||||
) ??
|
||||
const CircleAvatar(
|
||||
radius: 20,
|
||||
|
|
|
@ -74,10 +74,27 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = widget.scrollController ?? ScrollController();
|
||||
controller.addListener(_updateIsOnTop);
|
||||
|
||||
// only load the posts after the first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(loadPosts());
|
||||
|
@ -188,6 +205,7 @@ class _TimelineScreenState extends State<TimelineScreen> {
|
|||
),
|
||||
],
|
||||
CategorySelector(
|
||||
isOnTop: _isOnTop,
|
||||
filter: category,
|
||||
options: widget.options,
|
||||
onTapCategory: (categoryKey) {
|
||||
|
|
|
@ -17,7 +17,13 @@ class LocalTimelinePostService
|
|||
Future<TimelinePost> createPost(TimelinePost post) async {
|
||||
posts.add(
|
||||
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();
|
||||
|
@ -62,7 +68,13 @@ class LocalTimelinePostService
|
|||
for (var reaction in reactions) {
|
||||
updatedReactions.add(
|
||||
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(
|
||||
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(
|
||||
|
@ -179,11 +197,20 @@ class LocalTimelinePostService
|
|||
creatorId: 'test_user',
|
||||
title: 'Post 0',
|
||||
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',
|
||||
likes: 0,
|
||||
reaction: 0,
|
||||
createdAt: DateTime.now(),
|
||||
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(
|
||||
id: 'Post1',
|
||||
|
@ -197,7 +224,14 @@ class LocalTimelinePostService
|
|||
createdAt: DateTime.now(),
|
||||
reactionEnabled: false,
|
||||
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(
|
||||
id: 'Post2',
|
||||
|
@ -211,7 +245,14 @@ class LocalTimelinePostService
|
|||
createdAt: DateTime.now(),
|
||||
reactionEnabled: true,
|
||||
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',
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -3,55 +3,64 @@ import 'dart:math';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||
|
||||
class CategorySelector extends StatelessWidget {
|
||||
class CategorySelector extends StatefulWidget {
|
||||
const CategorySelector({
|
||||
required this.filter,
|
||||
required this.options,
|
||||
required this.onTapCategory,
|
||||
required this.isOnTop,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? filter;
|
||||
final TimelineOptions options;
|
||||
final void Function(String? categoryKey) onTapCategory;
|
||||
final bool isOnTop;
|
||||
|
||||
@override
|
||||
State<CategorySelector> createState() => _CategorySelectorState();
|
||||
}
|
||||
|
||||
class _CategorySelectorState extends State<CategorySelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (options.categoriesOptions.categoriesBuilder == null) {
|
||||
if (widget.options.categoriesOptions.categoriesBuilder == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
var categories = options.categoriesOptions.categoriesBuilder!(context);
|
||||
|
||||
var categories =
|
||||
widget.options.categoriesOptions.categoriesBuilder!(context);
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width:
|
||||
options.categoriesOptions.categorySelectorHorizontalPadding ??
|
||||
max(options.padding.horizontal - 4, 0),
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.padding.horizontal - 20, 0),
|
||||
),
|
||||
for (var category in categories) ...[
|
||||
options.categoriesOptions.categoryButtonBuilder?.call(
|
||||
widget.options.categoriesOptions.categoryButtonBuilder?.call(
|
||||
categoryKey: category.key,
|
||||
categoryName: category.title,
|
||||
onTap: () => onTapCategory(category.key),
|
||||
selected: filter == category.key,
|
||||
onTap: () => widget.onTapCategory(category.key),
|
||||
selected: widget.filter == category.key,
|
||||
) ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: CategorySelectorButton(
|
||||
isOnTop: widget.isOnTop,
|
||||
category: category,
|
||||
selected: filter == category.key,
|
||||
onTap: () => onTapCategory(category.key),
|
||||
selected: widget.filter == category.key,
|
||||
onTap: () => widget.onTapCategory(category.key),
|
||||
options: widget.options,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width:
|
||||
options.categoriesOptions.categorySelectorHorizontalPadding ??
|
||||
max(options.padding.horizontal - 4, 0),
|
||||
width: widget.options.categoriesOptions
|
||||
.categorySelectorHorizontalPadding ??
|
||||
max(widget.options.padding.horizontal - 4, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,50 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
|
||||
import 'package:flutter_timeline_view/flutter_timeline_view.dart';
|
||||
|
||||
class CategorySelectorButton extends StatelessWidget {
|
||||
const CategorySelectorButton({
|
||||
required this.category,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
required this.options,
|
||||
required this.isOnTop,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TimelineCategory category;
|
||||
final bool selected;
|
||||
final void Function() onTap;
|
||||
final TimelineOptions options;
|
||||
final bool isOnTop;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return TextButton(
|
||||
onPressed: onTap,
|
||||
style: ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const MaterialStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
vertical: 5,
|
||||
horizontal: 12,
|
||||
return AnimatedContainer(
|
||||
height: isOnTop ? 140 : 40,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: TextButton(
|
||||
onPressed: onTap,
|
||||
style: ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const MaterialStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
vertical: 5,
|
||||
horizontal: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
minimumSize: const MaterialStatePropertyAll(Size.zero),
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
selected ? theme.colorScheme.primary : theme.colorScheme.surface,
|
||||
),
|
||||
shape: const MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(45),
|
||||
fixedSize: MaterialStatePropertyAll(Size(140, isOnTop ? 140 : 20)),
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
selected ? theme.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
category.title,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment:
|
||||
isOnTop ? MainAxisAlignment.end : MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
category.title,
|
||||
style: (options.theme.textStyles.categoryTitleStyle ??
|
||||
theme.textTheme.labelLarge)
|
||||
?.copyWith(
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -30,46 +30,43 @@ class _ReactionBottomState extends State<ReactionBottom> {
|
|||
final TextEditingController _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
bottom: true,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
height: 48,
|
||||
child: widget.messageInputBuilder(
|
||||
_textEditingController,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
widget.translations.writeComment,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -69,7 +69,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
if (widget.post.creator!.imageUrl != null) ...[
|
||||
widget.options.userAvatarBuilder?.call(
|
||||
widget.post.creator!,
|
||||
40,
|
||||
28,
|
||||
) ??
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
|
@ -213,7 +213,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
icon: widget.options.theme.likeIcon ??
|
||||
Icon(
|
||||
widget.post.likedBy?.contains(widget.userId) ?? false
|
||||
? Icons.favorite
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline_outlined,
|
||||
),
|
||||
label: Text('${widget.post.likes}'),
|
||||
|
@ -240,7 +240,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
color: Colors.transparent,
|
||||
child: widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
Icons.thumb_up_rounded,
|
||||
Icons.favorite_rounded,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
|
@ -253,7 +253,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
|||
color: Colors.transparent,
|
||||
child: widget.options.theme.likedIcon ??
|
||||
Icon(
|
||||
Icons.thumb_up_rounded,
|
||||
Icons.favorite_outline,
|
||||
color: widget.options.theme.iconColor,
|
||||
size: widget.options.iconSize,
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue