mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 02:23:46 +02:00
fix: post creation, reaction like
This commit is contained in:
parent
eb953ede0d
commit
a8897242e7
11 changed files with 505 additions and 280 deletions
|
@ -405,4 +405,81 @@ class FirebaseTimelinePostService
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> likeReaction(
|
||||||
|
String userId,
|
||||||
|
TimelinePost post,
|
||||||
|
String reactionId,
|
||||||
|
) async {
|
||||||
|
// update the post with the new like
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reactions: post.reactions?.map(
|
||||||
|
(r) {
|
||||||
|
if (r.id == reactionId) {
|
||||||
|
return r.copyWith(
|
||||||
|
likedBy: (r.likedBy ?? [])..add(userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
posts = posts
|
||||||
|
.map(
|
||||||
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
|
await postRef.update({
|
||||||
|
'reactions': post.reactions
|
||||||
|
?.map(
|
||||||
|
(r) =>
|
||||||
|
r.id == reactionId ? r.copyWith(likedBy: r.likedBy ?? []) : r,
|
||||||
|
)
|
||||||
|
.map((e) => e.toJson())
|
||||||
|
.toList(),
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
return updatedPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> unlikeReaction(
|
||||||
|
String userId,
|
||||||
|
TimelinePost post,
|
||||||
|
String reactionId,
|
||||||
|
) async {
|
||||||
|
// update the post with the new like
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reactions: post.reactions?.map(
|
||||||
|
(r) {
|
||||||
|
if (r.id == reactionId) {
|
||||||
|
return r.copyWith(
|
||||||
|
likedBy: r.likedBy?..remove(userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
posts = posts
|
||||||
|
.map(
|
||||||
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
|
||||||
|
await postRef.update({
|
||||||
|
'reactions': post.reactions
|
||||||
|
?.map(
|
||||||
|
(r) => r.id == reactionId
|
||||||
|
? r.copyWith(likedBy: r.likedBy?..remove(userId))
|
||||||
|
: r,
|
||||||
|
)
|
||||||
|
.map((e) => e.toJson())
|
||||||
|
.toList(),
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
return updatedPost;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ class TimelinePostReaction {
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
this.creator,
|
this.creator,
|
||||||
this.createdAtString,
|
this.createdAtString,
|
||||||
|
this.likedBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory TimelinePostReaction.fromJson(
|
factory TimelinePostReaction.fromJson(
|
||||||
|
@ -31,6 +32,7 @@ class TimelinePostReaction {
|
||||||
imageUrl: json['image_url'] as String?,
|
imageUrl: json['image_url'] as String?,
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
createdAtString: json['created_at'] as String,
|
createdAtString: json['created_at'] as String,
|
||||||
|
likedBy: (json['liked_by'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The unique identifier of the reaction.
|
/// The unique identifier of the reaction.
|
||||||
|
@ -57,6 +59,8 @@ class TimelinePostReaction {
|
||||||
/// Reaction creation date as String with microseconds.
|
/// Reaction creation date as String with microseconds.
|
||||||
final String? createdAtString;
|
final String? createdAtString;
|
||||||
|
|
||||||
|
final List<String>? likedBy;
|
||||||
|
|
||||||
TimelinePostReaction copyWith({
|
TimelinePostReaction copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? postId,
|
String? postId,
|
||||||
|
@ -65,6 +69,7 @@ class TimelinePostReaction {
|
||||||
String? reaction,
|
String? reaction,
|
||||||
String? imageUrl,
|
String? imageUrl,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
List<String>? likedBy,
|
||||||
}) =>
|
}) =>
|
||||||
TimelinePostReaction(
|
TimelinePostReaction(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
@ -74,6 +79,7 @@ class TimelinePostReaction {
|
||||||
reaction: reaction ?? this.reaction,
|
reaction: reaction ?? this.reaction,
|
||||||
imageUrl: imageUrl ?? this.imageUrl,
|
imageUrl: imageUrl ?? this.imageUrl,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
likedBy: likedBy ?? this.likedBy,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||||
|
@ -82,6 +88,7 @@ class TimelinePostReaction {
|
||||||
'reaction': reaction,
|
'reaction': reaction,
|
||||||
'image_url': imageUrl,
|
'image_url': imageUrl,
|
||||||
'created_at': createdAt.toIso8601String(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
'liked_by': likedBy,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -91,6 +98,7 @@ class TimelinePostReaction {
|
||||||
'reaction': reaction,
|
'reaction': reaction,
|
||||||
'image_url': imageUrl,
|
'image_url': imageUrl,
|
||||||
'created_at': createdAtString,
|
'created_at': createdAtString,
|
||||||
|
'liked_by': likedBy,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,4 +32,14 @@ abstract class TimelinePostService with ChangeNotifier {
|
||||||
|
|
||||||
Future<List<TimelineCategory>> fetchCategories();
|
Future<List<TimelineCategory>> fetchCategories();
|
||||||
Future<bool> addCategory(TimelineCategory category);
|
Future<bool> addCategory(TimelineCategory category);
|
||||||
|
Future<TimelinePost> likeReaction(
|
||||||
|
String userId,
|
||||||
|
TimelinePost post,
|
||||||
|
String reactionId,
|
||||||
|
);
|
||||||
|
Future<TimelinePost> unlikeReaction(
|
||||||
|
String userId,
|
||||||
|
TimelinePost post,
|
||||||
|
String reactionId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,9 @@ class TimelineTranslations {
|
||||||
required this.addCategorySubmitButton,
|
required this.addCategorySubmitButton,
|
||||||
required this.addCategoryCancelButtton,
|
required this.addCategoryCancelButtton,
|
||||||
required this.addCategoryHintText,
|
required this.addCategoryHintText,
|
||||||
|
required this.addCategoryErrorText,
|
||||||
|
required this.titleErrorText,
|
||||||
|
required this.contentErrorText,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Default translations for the timeline component view
|
/// Default translations for the timeline component view
|
||||||
|
@ -100,6 +103,9 @@ class TimelineTranslations {
|
||||||
this.addCategorySubmitButton = 'Add category',
|
this.addCategorySubmitButton = 'Add category',
|
||||||
this.addCategoryCancelButtton = 'Cancel',
|
this.addCategoryCancelButtton = 'Cancel',
|
||||||
this.addCategoryHintText = 'Category name...',
|
this.addCategoryHintText = 'Category name...',
|
||||||
|
this.addCategoryErrorText = 'Please enter a category name',
|
||||||
|
this.titleErrorText = 'Please enter a title',
|
||||||
|
this.contentErrorText = 'Please enter content',
|
||||||
});
|
});
|
||||||
|
|
||||||
final String noPosts;
|
final String noPosts;
|
||||||
|
@ -117,6 +123,8 @@ class TimelineTranslations {
|
||||||
|
|
||||||
final String titleHintText;
|
final String titleHintText;
|
||||||
final String contentHintText;
|
final String contentHintText;
|
||||||
|
final String titleErrorText;
|
||||||
|
final String contentErrorText;
|
||||||
|
|
||||||
final String deletePost;
|
final String deletePost;
|
||||||
final String deleteConfirmationTitle;
|
final String deleteConfirmationTitle;
|
||||||
|
@ -147,6 +155,7 @@ class TimelineTranslations {
|
||||||
final String addCategorySubmitButton;
|
final String addCategorySubmitButton;
|
||||||
final String addCategoryCancelButtton;
|
final String addCategoryCancelButtton;
|
||||||
final String addCategoryHintText;
|
final String addCategoryHintText;
|
||||||
|
final String addCategoryErrorText;
|
||||||
|
|
||||||
final String yes;
|
final String yes;
|
||||||
final String no;
|
final String no;
|
||||||
|
@ -194,6 +203,9 @@ class TimelineTranslations {
|
||||||
String? addCategorySubmitButton,
|
String? addCategorySubmitButton,
|
||||||
String? addCategoryCancelButtton,
|
String? addCategoryCancelButtton,
|
||||||
String? addCategoryHintText,
|
String? addCategoryHintText,
|
||||||
|
String? addCategoryErrorText,
|
||||||
|
String? titleErrorText,
|
||||||
|
String? contentErrorText,
|
||||||
}) =>
|
}) =>
|
||||||
TimelineTranslations(
|
TimelineTranslations(
|
||||||
noPosts: noPosts ?? this.noPosts,
|
noPosts: noPosts ?? this.noPosts,
|
||||||
|
@ -244,5 +256,8 @@ class TimelineTranslations {
|
||||||
addCategoryHintText: addCategoryHintText ?? this.addCategoryHintText,
|
addCategoryHintText: addCategoryHintText ?? this.addCategoryHintText,
|
||||||
createCategoryPopuptitle:
|
createCategoryPopuptitle:
|
||||||
createCategoryPopuptitle ?? this.createCategoryPopuptitle,
|
createCategoryPopuptitle ?? this.createCategoryPopuptitle,
|
||||||
|
addCategoryErrorText: addCategoryErrorText ?? this.addCategoryErrorText,
|
||||||
|
titleErrorText: titleErrorText ?? this.titleErrorText,
|
||||||
|
contentErrorText: contentErrorText ?? this.contentErrorText,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,48 +53,23 @@ class _TimelinePostCreationScreenState
|
||||||
TextEditingController titleController = TextEditingController();
|
TextEditingController titleController = TextEditingController();
|
||||||
TextEditingController contentController = TextEditingController();
|
TextEditingController contentController = TextEditingController();
|
||||||
Uint8List? image;
|
Uint8List? image;
|
||||||
bool editingDone = false;
|
|
||||||
bool allowComments = false;
|
bool allowComments = false;
|
||||||
|
bool titleIsValid = false;
|
||||||
|
bool contentIsValid = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
titleController.addListener(_listenForInputs);
|
||||||
|
contentController.addListener(_listenForInputs);
|
||||||
super.initState();
|
super.initState();
|
||||||
titleController.addListener(checkIfEditingDone);
|
|
||||||
contentController.addListener(checkIfEditingDone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _listenForInputs() {
|
||||||
void dispose() {
|
titleIsValid = titleController.text.isNotEmpty;
|
||||||
titleController.dispose();
|
contentIsValid = contentController.text.isNotEmpty;
|
||||||
contentController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkIfEditingDone() {
|
var formkey = GlobalKey<FormState>();
|
||||||
setState(() {
|
|
||||||
editingDone =
|
|
||||||
titleController.text.isNotEmpty && contentController.text.isNotEmpty;
|
|
||||||
if (widget.options.requireImageForPost) {
|
|
||||||
editingDone = editingDone && image != null;
|
|
||||||
}
|
|
||||||
if (widget.options.minTitleLength != null) {
|
|
||||||
editingDone = editingDone &&
|
|
||||||
titleController.text.length >= widget.options.minTitleLength!;
|
|
||||||
}
|
|
||||||
if (widget.options.maxTitleLength != null) {
|
|
||||||
editingDone = editingDone &&
|
|
||||||
titleController.text.length <= widget.options.maxTitleLength!;
|
|
||||||
}
|
|
||||||
if (widget.options.minContentLength != null) {
|
|
||||||
editingDone = editingDone &&
|
|
||||||
contentController.text.length >= widget.options.minContentLength!;
|
|
||||||
}
|
|
||||||
if (widget.options.maxContentLength != null) {
|
|
||||||
editingDone = editingDone &&
|
|
||||||
contentController.text.length <= widget.options.maxContentLength!;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -127,251 +102,274 @@ class _TimelinePostCreationScreenState
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.options.paddings.mainPadding,
|
padding: widget.options.paddings.mainPadding,
|
||||||
child: Column(
|
child: Form(
|
||||||
mainAxisSize: MainAxisSize.max,
|
key: formkey,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.max,
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
widget.options.translations.title,
|
children: [
|
||||||
style: theme.textTheme.titleMedium,
|
Text(
|
||||||
),
|
widget.options.translations.title,
|
||||||
const SizedBox(
|
style: theme.textTheme.titleMedium,
|
||||||
height: 4,
|
),
|
||||||
),
|
const SizedBox(
|
||||||
widget.options.textInputBuilder?.call(
|
height: 4,
|
||||||
titleController,
|
),
|
||||||
null,
|
widget.options.textInputBuilder?.call(
|
||||||
'',
|
titleController,
|
||||||
) ??
|
null,
|
||||||
PostCreationTextfield(
|
'',
|
||||||
controller: titleController,
|
) ??
|
||||||
hintText: widget.options.translations.titleHintText,
|
PostCreationTextfield(
|
||||||
textMaxLength: widget.options.maxTitleLength,
|
controller: titleController,
|
||||||
decoration: widget.options.titleInputDecoration,
|
hintText: widget.options.translations.titleHintText,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textMaxLength: widget.options.maxTitleLength,
|
||||||
expands: null,
|
decoration: widget.options.titleInputDecoration,
|
||||||
minLines: null,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
maxLines: 1,
|
expands: null,
|
||||||
),
|
minLines: null,
|
||||||
const SizedBox(height: 16),
|
maxLines: 1,
|
||||||
Text(
|
validator: (value) {
|
||||||
widget.options.translations.content,
|
if (value == null || value.isEmpty) {
|
||||||
style: theme.textTheme.titleMedium,
|
return widget.options.translations.titleErrorText;
|
||||||
),
|
}
|
||||||
Text(
|
if (value.trim().isEmpty) {
|
||||||
widget.options.translations.contentDescription,
|
return widget.options.translations.titleErrorText;
|
||||||
style: theme.textTheme.bodySmall,
|
}
|
||||||
),
|
return null;
|
||||||
const SizedBox(
|
},
|
||||||
height: 4,
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
PostCreationTextfield(
|
Text(
|
||||||
controller: contentController,
|
widget.options.translations.content,
|
||||||
hintText: widget.options.translations.contentHintText,
|
style: theme.textTheme.titleMedium,
|
||||||
textMaxLength: null,
|
),
|
||||||
decoration: widget.options.contentInputDecoration,
|
Text(
|
||||||
textCapitalization: TextCapitalization.sentences,
|
widget.options.translations.contentDescription,
|
||||||
expands: false,
|
style: theme.textTheme.bodySmall,
|
||||||
minLines: null,
|
),
|
||||||
maxLines: null,
|
const SizedBox(
|
||||||
),
|
height: 4,
|
||||||
const SizedBox(
|
),
|
||||||
height: 16,
|
PostCreationTextfield(
|
||||||
),
|
controller: contentController,
|
||||||
Text(
|
hintText: widget.options.translations.contentHintText,
|
||||||
widget.options.translations.uploadImage,
|
textMaxLength: null,
|
||||||
style: theme.textTheme.titleMedium,
|
decoration: widget.options.contentInputDecoration,
|
||||||
),
|
textCapitalization: TextCapitalization.sentences,
|
||||||
Text(
|
expands: false,
|
||||||
widget.options.translations.uploadImageDescription,
|
minLines: null,
|
||||||
style: theme.textTheme.bodySmall,
|
maxLines: null,
|
||||||
),
|
validator: (value) {
|
||||||
const SizedBox(
|
if (value == null || value.isEmpty) {
|
||||||
height: 8,
|
return widget.options.translations.contentErrorText;
|
||||||
),
|
}
|
||||||
Stack(
|
if (value.trim().isEmpty) {
|
||||||
children: [
|
return widget.options.translations.contentErrorText;
|
||||||
GestureDetector(
|
}
|
||||||
onTap: () async {
|
return null;
|
||||||
var result = await showModalBottomSheet<Uint8List?>(
|
},
|
||||||
context: context,
|
),
|
||||||
builder: (context) => Container(
|
const SizedBox(
|
||||||
padding: const EdgeInsets.all(8.0),
|
height: 16,
|
||||||
color: theme.colorScheme.surface,
|
),
|
||||||
child: ImagePicker(
|
Text(
|
||||||
imagePickerConfig: widget.options.imagePickerConfig,
|
widget.options.translations.uploadImage,
|
||||||
imagePickerTheme: widget.options.imagePickerTheme ??
|
style: theme.textTheme.titleMedium,
|
||||||
ImagePickerTheme(
|
),
|
||||||
titleAlignment: TextAlign.center,
|
Text(
|
||||||
title: ' Do you want to upload a file'
|
widget.options.translations.uploadImageDescription,
|
||||||
' or take a picture? ',
|
style: theme.textTheme.bodySmall,
|
||||||
titleTextSize:
|
),
|
||||||
theme.textTheme.titleMedium!.fontSize!,
|
const SizedBox(
|
||||||
font:
|
height: 8,
|
||||||
theme.textTheme.titleMedium!.fontFamily!,
|
),
|
||||||
iconSize: 40,
|
Stack(
|
||||||
selectImageText: 'UPLOAD FILE',
|
children: [
|
||||||
makePhotoText: 'TAKE PICTURE',
|
GestureDetector(
|
||||||
selectImageIcon: const Icon(
|
onTap: () async {
|
||||||
size: 40,
|
var result = await showModalBottomSheet<Uint8List?>(
|
||||||
Icons.insert_drive_file,
|
context: context,
|
||||||
|
builder: (context) => Container(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
child: ImagePicker(
|
||||||
|
imagePickerConfig:
|
||||||
|
widget.options.imagePickerConfig,
|
||||||
|
imagePickerTheme: widget
|
||||||
|
.options.imagePickerTheme ??
|
||||||
|
ImagePickerTheme(
|
||||||
|
titleAlignment: TextAlign.center,
|
||||||
|
title: ' Do you want to upload a file'
|
||||||
|
' or take a picture? ',
|
||||||
|
titleTextSize:
|
||||||
|
theme.textTheme.titleMedium!.fontSize!,
|
||||||
|
font: theme
|
||||||
|
.textTheme.titleMedium!.fontFamily!,
|
||||||
|
iconSize: 40,
|
||||||
|
selectImageText: 'UPLOAD FILE',
|
||||||
|
makePhotoText: 'TAKE PICTURE',
|
||||||
|
selectImageIcon: const Icon(
|
||||||
|
size: 40,
|
||||||
|
Icons.insert_drive_file,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
customButton: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Cancel',
|
||||||
|
style: theme.textTheme.bodyMedium!.copyWith(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
customButton: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Cancel',
|
|
||||||
style: theme.textTheme.bodyMedium!.copyWith(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
if (result != null) {
|
||||||
if (result != null) {
|
setState(() {
|
||||||
setState(() {
|
image = result;
|
||||||
image = result;
|
});
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
checkIfEditingDone();
|
child: ClipRRect(
|
||||||
},
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
child: ClipRRect(
|
child: image != null
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
? Image.memory(
|
||||||
child: image != null
|
image!,
|
||||||
? Image.memory(
|
|
||||||
image!,
|
|
||||||
width: double.infinity,
|
|
||||||
height: 150.0,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
// give it a rounded border
|
|
||||||
)
|
|
||||||
: DottedBorder(
|
|
||||||
dashPattern: const [4, 4],
|
|
||||||
radius: const Radius.circular(8.0),
|
|
||||||
color: theme.textTheme.displayMedium?.color ??
|
|
||||||
Colors.white,
|
|
||||||
child: const SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 150.0,
|
height: 150.0,
|
||||||
child: Icon(
|
fit: BoxFit.cover,
|
||||||
Icons.image,
|
// give it a rounded border
|
||||||
size: 50,
|
)
|
||||||
|
: 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 an image is selected, show a delete button
|
if (image != null) ...[
|
||||||
if (image != null) ...[
|
Positioned(
|
||||||
Positioned(
|
top: 8,
|
||||||
top: 8,
|
right: 8,
|
||||||
right: 8,
|
child: GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: () {
|
||||||
onTap: () {
|
setState(() {
|
||||||
setState(() {
|
image = null;
|
||||||
image = null;
|
});
|
||||||
});
|
},
|
||||||
checkIfEditingDone();
|
child: Container(
|
||||||
},
|
decoration: BoxDecoration(
|
||||||
child: Container(
|
color: Colors.black.withOpacity(0.5),
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
color: Colors.black.withOpacity(0.5),
|
),
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
child: const Icon(
|
||||||
),
|
Icons.delete,
|
||||||
child: const Icon(
|
color: Colors.white,
|
||||||
Icons.delete,
|
),
|
||||||
color: Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.commentsTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.allowCommentsDescription,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Checkbox(
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity:
|
||||||
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
activeColor: theme.colorScheme.primary,
|
||||||
|
value: allowComments,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
allowComments = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.yes,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 32,
|
||||||
|
),
|
||||||
|
Checkbox(
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity:
|
||||||
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
activeColor: theme.colorScheme.primary,
|
||||||
|
value: !allowComments,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
allowComments = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.options.translations.no,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.commentsTitle,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.allowCommentsDescription,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Checkbox(
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
visualDensity:
|
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
activeColor: theme.colorScheme.primary,
|
|
||||||
value: allowComments,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
allowComments = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.yes,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 32,
|
|
||||||
),
|
|
||||||
Checkbox(
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
visualDensity:
|
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
activeColor: theme.colorScheme.primary,
|
|
||||||
value: !allowComments,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
allowComments = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.options.translations.no,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 120),
|
|
||||||
SafeArea(
|
|
||||||
bottom: true,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: widget.options.buttonBuilder?.call(
|
|
||||||
context,
|
|
||||||
onPostCreated,
|
|
||||||
widget.options.translations.checkPost,
|
|
||||||
enabled: editingDone,
|
|
||||||
) ??
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 48),
|
|
||||||
child: DefaultFilledButton(
|
|
||||||
onPressed: editingDone
|
|
||||||
? () async {
|
|
||||||
await onPostCreated();
|
|
||||||
await widget.service.postService
|
|
||||||
.fetchPosts(null);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
buttonText: widget.enablePostOverviewScreen
|
|
||||||
? widget.options.translations.checkPost
|
|
||||||
: widget.options.translations.postCreation,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 120),
|
||||||
],
|
SafeArea(
|
||||||
|
bottom: true,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: widget.options.buttonBuilder?.call(
|
||||||
|
context,
|
||||||
|
onPostCreated,
|
||||||
|
widget.options.translations.checkPost,
|
||||||
|
enabled: formkey.currentState!.validate(),
|
||||||
|
) ??
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 48),
|
||||||
|
child: DefaultFilledButton(
|
||||||
|
onPressed: titleIsValid && contentIsValid
|
||||||
|
? () async {
|
||||||
|
if (formkey.currentState!.validate()) {
|
||||||
|
await onPostCreated();
|
||||||
|
await widget.service.postService
|
||||||
|
.fetchPosts(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
buttonText: widget.enablePostOverviewScreen
|
||||||
|
? widget.options.translations.checkPost
|
||||||
|
: widget.options.translations.postCreation,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -344,13 +344,19 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: isLikedByUser
|
||||||
isLikedByUser
|
? widget.options.theme.likedIcon ??
|
||||||
? Icons.favorite_rounded
|
Icon(
|
||||||
: Icons.favorite_outline_outlined,
|
Icons.favorite_rounded,
|
||||||
color: widget.options.theme.iconColor,
|
color: widget.options.theme.iconColor,
|
||||||
size: widget.options.iconSize,
|
size: widget.options.iconSize,
|
||||||
),
|
)
|
||||||
|
: widget.options.theme.likeIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.favorite_outline_outlined,
|
||||||
|
color: widget.options.theme.iconColor,
|
||||||
|
size: widget.options.iconSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (post.reactionEnabled)
|
if (post.reactionEnabled)
|
||||||
|
@ -410,7 +416,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
if (post.reactionEnabled || widget.isOverviewScreen != null
|
if (post.reactionEnabled && widget.isOverviewScreen != null
|
||||||
? !widget.isOverviewScreen!
|
? !widget.isOverviewScreen!
|
||||||
: false) ...[
|
: false) ...[
|
||||||
Text(
|
Text(
|
||||||
|
@ -544,6 +550,55 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
var isLikedByUser =
|
||||||
|
reaction.likedBy?.contains(widget.userId) ??
|
||||||
|
false;
|
||||||
|
return IconButton(
|
||||||
|
padding: const EdgeInsets.only(left: 12),
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () async {
|
||||||
|
if (isLikedByUser) {
|
||||||
|
updatePost(
|
||||||
|
await widget.service.postService
|
||||||
|
.unlikeReaction(
|
||||||
|
widget.userId,
|
||||||
|
post,
|
||||||
|
reaction.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
} else {
|
||||||
|
updatePost(
|
||||||
|
await widget.service.postService
|
||||||
|
.likeReaction(
|
||||||
|
widget.userId,
|
||||||
|
post,
|
||||||
|
reaction.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: isLikedByUser
|
||||||
|
? widget.options.theme.likedIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.favorite_rounded,
|
||||||
|
color:
|
||||||
|
widget.options.theme.iconColor,
|
||||||
|
size: 14,
|
||||||
|
)
|
||||||
|
: widget.options.theme.likeIcon ??
|
||||||
|
Icon(
|
||||||
|
Icons.favorite_outline_outlined,
|
||||||
|
color:
|
||||||
|
widget.options.theme.iconColor,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -163,6 +163,9 @@ class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
|
||||||
PostCreationTextfield(
|
PostCreationTextfield(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
hintText: widget.options.translations.addCategoryHintText,
|
hintText: widget.options.translations.addCategoryHintText,
|
||||||
|
validator: (p0) => p0!.isEmpty
|
||||||
|
? widget.options.translations.addCategoryErrorText
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
|
@ -180,7 +183,7 @@ class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
if (mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
buttonText:
|
buttonText:
|
||||||
widget.options.translations.addCategorySubmitButton,
|
widget.options.translations.addCategorySubmitButton,
|
||||||
|
|
|
@ -272,4 +272,60 @@ class LocalTimelinePostService
|
||||||
|
|
||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> likeReaction(
|
||||||
|
String userId,
|
||||||
|
TimelinePost post,
|
||||||
|
String reactionId,
|
||||||
|
) async {
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reactions: post.reactions?.map(
|
||||||
|
(r) {
|
||||||
|
if (r.id == reactionId) {
|
||||||
|
return r.copyWith(
|
||||||
|
likedBy: (r.likedBy ?? [])..add(userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
posts = posts
|
||||||
|
.map(
|
||||||
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
return updatedPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TimelinePost> unlikeReaction(
|
||||||
|
String userId,
|
||||||
|
TimelinePost post,
|
||||||
|
String reactionId,
|
||||||
|
) async {
|
||||||
|
var updatedPost = post.copyWith(
|
||||||
|
reactions: post.reactions?.map(
|
||||||
|
(r) {
|
||||||
|
if (r.id == reactionId) {
|
||||||
|
return r.copyWith(
|
||||||
|
likedBy: r.likedBy?..remove(userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
posts = posts
|
||||||
|
.map(
|
||||||
|
(p) => p.id == post.id ? updatedPost : p,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
return updatedPost;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,13 @@ class DefaultFilledButton extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
return FilledButton(
|
return FilledButton(
|
||||||
style: ButtonStyle(
|
style: onPressed != null
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
? ButtonStyle(
|
||||||
theme.colorScheme.primary,
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
),
|
theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
|
|
|
@ -4,6 +4,7 @@ class PostCreationTextfield extends StatelessWidget {
|
||||||
const PostCreationTextfield({
|
const PostCreationTextfield({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
|
required this.validator,
|
||||||
super.key,
|
super.key,
|
||||||
this.textMaxLength,
|
this.textMaxLength,
|
||||||
this.decoration,
|
this.decoration,
|
||||||
|
@ -22,10 +23,12 @@ class PostCreationTextfield extends StatelessWidget {
|
||||||
final bool? expands;
|
final bool? expands;
|
||||||
final int? minLines;
|
final int? minLines;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
return TextField(
|
return TextFormField(
|
||||||
|
validator: validator,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
maxLength: textMaxLength,
|
maxLength: textMaxLength,
|
||||||
|
|
|
@ -315,9 +315,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
|
||||||
] else ...[
|
] else ...[
|
||||||
Text(
|
Text(
|
||||||
'${widget.post.likes} '
|
'${widget.post.likes} '
|
||||||
'${widget.post.likes > 1 ?
|
'${widget.post.likes > 1 ? widget.options.translations.multipleLikesTitle : widget.options.translations.oneLikeTitle}',
|
||||||
widget.options.translations.multipleLikesTitle :
|
|
||||||
widget.options.translations.oneLikeTitle}',
|
|
||||||
style:
|
style:
|
||||||
widget.options.theme.textStyles.listPostLikeTitleAndAmount ??
|
widget.options.theme.textStyles.listPostLikeTitleAndAmount ??
|
||||||
theme.textTheme.titleSmall!.copyWith(
|
theme.textTheme.titleSmall!.copyWith(
|
||||||
|
|
Loading…
Reference in a new issue