✨ Post tags
This commit is contained in:
parent
0361f031db
commit
4deff5a920
@ -103,30 +103,32 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
originalPost: originalPost,
|
||||
forwardedPost: effectiveForwardedPost,
|
||||
repliedPost: effectiveRepliedPost,
|
||||
postType: 0, // Regular post type
|
||||
),
|
||||
[originalPost, effectiveForwardedPost, effectiveRepliedPost],
|
||||
);
|
||||
|
||||
// Add a listener to the entire state to trigger rebuilds
|
||||
final stateNotifier = useMemoized(
|
||||
() => Listenable.merge([
|
||||
state.titleController,
|
||||
state.descriptionController,
|
||||
state.contentController,
|
||||
state.visibility,
|
||||
state.attachments,
|
||||
state.attachmentProgress,
|
||||
state.currentPublisher,
|
||||
state.submitting,
|
||||
]),
|
||||
[state]);
|
||||
() => Listenable.merge([
|
||||
state.titleController,
|
||||
state.descriptionController,
|
||||
state.contentController,
|
||||
state.visibility,
|
||||
state.attachments,
|
||||
state.attachmentProgress,
|
||||
state.currentPublisher,
|
||||
state.submitting,
|
||||
]),
|
||||
[state],
|
||||
);
|
||||
useListenable(stateNotifier);
|
||||
|
||||
// Start auto-save when component mounts
|
||||
useEffect(() {
|
||||
if (originalPost == null) {
|
||||
// Only auto-save for new posts, not edits
|
||||
state.startAutoSave(ref, postType: 0);
|
||||
state.startAutoSave(ref);
|
||||
}
|
||||
return () => state.stopAutoSave();
|
||||
}, [state]);
|
||||
@ -165,13 +167,18 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
final drafts = ref.read(composeStorageNotifierProvider);
|
||||
if (drafts.isNotEmpty) {
|
||||
final mostRecentDraft = drafts.values.reduce(
|
||||
(a, b) => (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) ? a : b,
|
||||
(a, b) =>
|
||||
(a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0))
|
||||
? a
|
||||
: b,
|
||||
);
|
||||
|
||||
// Only load if the draft has meaningful content
|
||||
if (mostRecentDraft.content?.isNotEmpty == true || mostRecentDraft.title?.isNotEmpty == true) {
|
||||
if (mostRecentDraft.content?.isNotEmpty == true ||
|
||||
mostRecentDraft.title?.isNotEmpty == true) {
|
||||
state.titleController.text = mostRecentDraft.title ?? '';
|
||||
state.descriptionController.text = mostRecentDraft.description ?? '';
|
||||
state.descriptionController.text =
|
||||
mostRecentDraft.description ?? '';
|
||||
state.contentController.text = mostRecentDraft.content ?? '';
|
||||
state.visibility.value = mostRecentDraft.visibility;
|
||||
}
|
||||
@ -298,7 +305,8 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
state.titleController.text = draft.title ?? '';
|
||||
state.descriptionController.text =
|
||||
draft.description ?? '';
|
||||
state.contentController.text = draft.content ?? '';
|
||||
state.contentController.text =
|
||||
draft.content ?? '';
|
||||
state.visibility.value = draft.visibility;
|
||||
}
|
||||
},
|
||||
@ -322,14 +330,13 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
state.submitting.value
|
||||
? null
|
||||
: () => ComposeLogic.performAction(
|
||||
ref,
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
postType: 0, // Regular post type
|
||||
),
|
||||
ref,
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
),
|
||||
icon:
|
||||
state.submitting.value
|
||||
? SizedBox(
|
||||
@ -341,9 +348,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
),
|
||||
).center()
|
||||
: Icon(
|
||||
originalPost != null
|
||||
? Symbols.edit
|
||||
: Symbols.upload,
|
||||
originalPost != null ? Symbols.edit : Symbols.upload,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -405,7 +410,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
postType: 0, // Regular post type
|
||||
),
|
||||
child: TextField(
|
||||
controller: state.contentController,
|
||||
|
@ -60,7 +60,10 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
final state = useMemoized(
|
||||
() => ComposeLogic.createState(originalPost: originalPost),
|
||||
() => ComposeLogic.createState(
|
||||
originalPost: originalPost,
|
||||
postType: 1, // Article type
|
||||
),
|
||||
[originalPost],
|
||||
);
|
||||
|
||||
@ -70,7 +73,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
if (originalPost == null) {
|
||||
// Only auto-save for new articles, not edits
|
||||
autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state);
|
||||
});
|
||||
}
|
||||
return () {
|
||||
@ -78,7 +81,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
state.stopAutoSave();
|
||||
// Save final draft before disposing
|
||||
if (originalPost == null) {
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state);
|
||||
}
|
||||
ComposeLogic.dispose(state);
|
||||
autoSaveTimer?.cancel();
|
||||
@ -362,7 +365,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
return PopScope(
|
||||
onPopInvoked: (_) {
|
||||
if (originalPost == null) {
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state);
|
||||
}
|
||||
},
|
||||
child: AppScaffold(
|
||||
@ -410,7 +413,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.save),
|
||||
onPressed: () => ComposeLogic.saveDraft(ref, state, postType: 1),
|
||||
onPressed: () => ComposeLogic.saveDraft(ref, state),
|
||||
tooltip: 'saveDraft'.tr(),
|
||||
),
|
||||
IconButton(
|
||||
@ -437,7 +440,6 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
postType: 1, // Article type
|
||||
),
|
||||
icon:
|
||||
submitting
|
||||
@ -530,18 +532,17 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
if (isPaste && isModifierPressed) {
|
||||
ComposeLogic.handlePaste(state);
|
||||
} else if (isSave && isModifierPressed) {
|
||||
ComposeLogic.saveDraft(ref, state, postType: 1);
|
||||
ComposeLogic.saveDraft(ref, state);
|
||||
ComposeLogic.saveDraft(ref, state);
|
||||
} else if (isSubmit && isModifierPressed && !state.submitting.value) {
|
||||
ComposeLogic.performAction(
|
||||
ref,
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
postType: 1, // Article type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to save article draft
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ class ChipTagInputField extends StatelessWidget {
|
||||
decoration: InputDecoration(
|
||||
label: Text(labelText).tr(),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
hintText: inputFieldValues.tags.isNotEmpty ? '' : hintText.tr(),
|
||||
errorText: inputFieldValues.error,
|
||||
prefixIconConstraints: BoxConstraints(
|
||||
@ -51,9 +52,7 @@ class ChipTagInputField extends StatelessWidget {
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0,
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 5),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0,
|
||||
vertical: 5.0,
|
||||
@ -72,11 +71,8 @@ class ChipTagInputField extends StatelessWidget {
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
//print("$tag selected");
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
const Gap(4),
|
||||
InkWell(
|
||||
child: const Icon(
|
||||
Icons.cancel,
|
||||
|
@ -27,9 +27,10 @@ class ComposeState {
|
||||
final ValueNotifier<Map<int, double>> attachmentProgress;
|
||||
final ValueNotifier<SnPublisher?> currentPublisher;
|
||||
final ValueNotifier<bool> submitting;
|
||||
final StringTagController tagsController;
|
||||
final StringTagController categoriesController;
|
||||
StringTagController tagsController;
|
||||
StringTagController categoriesController;
|
||||
final String draftId;
|
||||
int postType;
|
||||
Timer? _autoSaveTimer;
|
||||
|
||||
ComposeState({
|
||||
@ -44,12 +45,13 @@ class ComposeState {
|
||||
required this.tagsController,
|
||||
required this.categoriesController,
|
||||
required this.draftId,
|
||||
this.postType = 0,
|
||||
});
|
||||
|
||||
void startAutoSave(WidgetRef ref, {int postType = 0}) {
|
||||
void startAutoSave(WidgetRef ref) {
|
||||
_autoSaveTimer?.cancel();
|
||||
_autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, this, postType: postType);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, this);
|
||||
});
|
||||
}
|
||||
|
||||
@ -65,6 +67,7 @@ class ComposeLogic {
|
||||
SnPost? forwardedPost,
|
||||
SnPost? repliedPost,
|
||||
String? draftId,
|
||||
int postType = 0,
|
||||
}) {
|
||||
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final tagsController = StringTagController();
|
||||
@ -110,10 +113,11 @@ class ComposeLogic {
|
||||
tagsController: tagsController,
|
||||
categoriesController: categoriesController,
|
||||
draftId: id,
|
||||
postType: postType,
|
||||
);
|
||||
}
|
||||
|
||||
static ComposeState createStateFromDraft(SnPost draft) {
|
||||
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
|
||||
final tagsController = StringTagController();
|
||||
final categoriesController = StringTagController();
|
||||
for (var x in draft.tags) {
|
||||
@ -136,14 +140,11 @@ class ComposeLogic {
|
||||
tagsController: tagsController,
|
||||
categoriesController: categoriesController,
|
||||
draftId: draft.id,
|
||||
postType: postType,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> saveDraft(
|
||||
WidgetRef ref,
|
||||
ComposeState state, {
|
||||
int postType = 0,
|
||||
}) async {
|
||||
static Future<void> saveDraft(WidgetRef ref, ComposeState state) async {
|
||||
final hasContent =
|
||||
state.titleController.text.trim().isNotEmpty ||
|
||||
state.descriptionController.text.trim().isNotEmpty ||
|
||||
@ -175,7 +176,7 @@ class ComposeLogic {
|
||||
baseUrl: baseUrl,
|
||||
filename:
|
||||
attachment.data.name ??
|
||||
(postType == 1 ? 'Article media' : 'Post media'),
|
||||
(state.postType == 1 ? 'Article media' : 'Post media'),
|
||||
mimetype:
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(attachment.type),
|
||||
@ -202,7 +203,7 @@ class ComposeLogic {
|
||||
publishedAt: DateTime.now(),
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: postType,
|
||||
type: state.postType,
|
||||
meta: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
@ -252,9 +253,8 @@ class ComposeLogic {
|
||||
|
||||
static Future<void> saveDraftWithoutUpload(
|
||||
WidgetRef ref,
|
||||
ComposeState state, {
|
||||
int postType = 0,
|
||||
}) async {
|
||||
ComposeState state,
|
||||
) async {
|
||||
final hasContent =
|
||||
state.titleController.text.trim().isNotEmpty ||
|
||||
state.descriptionController.text.trim().isNotEmpty ||
|
||||
@ -279,7 +279,7 @@ class ComposeLogic {
|
||||
publishedAt: DateTime.now(),
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: postType,
|
||||
type: state.postType,
|
||||
meta: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
@ -333,54 +333,7 @@ class ComposeLogic {
|
||||
BuildContext context,
|
||||
) async {
|
||||
try {
|
||||
final draft = SnPost(
|
||||
id: state.draftId,
|
||||
title: state.titleController.text,
|
||||
description: state.descriptionController.text,
|
||||
language: null,
|
||||
editedAt: null,
|
||||
publishedAt: DateTime.now(),
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: 0,
|
||||
meta: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
upvotes: 0,
|
||||
downvotes: 0,
|
||||
repliesCount: 0,
|
||||
threadedPostId: null,
|
||||
threadedPost: null,
|
||||
repliedPostId: null,
|
||||
repliedPost: null,
|
||||
forwardedPostId: null,
|
||||
forwardedPost: null,
|
||||
attachments: [], // TODO: Handle attachments
|
||||
publisher: SnPublisher(
|
||||
id: '',
|
||||
type: 0,
|
||||
name: '',
|
||||
nick: '',
|
||||
picture: null,
|
||||
background: null,
|
||||
account: null,
|
||||
accountId: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
realmId: null,
|
||||
verification: null,
|
||||
),
|
||||
reactions: [],
|
||||
tags: [],
|
||||
categories: [],
|
||||
collections: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
);
|
||||
|
||||
await ref.read(composeStorageNotifierProvider.notifier).saveDraft(draft);
|
||||
await saveDraft(ref, state);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBar('draftSaved'.tr());
|
||||
@ -535,7 +488,6 @@ class ComposeLogic {
|
||||
SnPost? originalPost,
|
||||
SnPost? repliedPost,
|
||||
SnPost? forwardedPost,
|
||||
int? postType, // 0 for regular post, 1 for article
|
||||
}) async {
|
||||
if (state.submitting.value) return;
|
||||
|
||||
@ -581,7 +533,7 @@ class ComposeLogic {
|
||||
.where((e) => e.isOnCloud)
|
||||
.map((e) => e.data.id)
|
||||
.toList(),
|
||||
if (postType != null) 'type': postType,
|
||||
'type': state.postType,
|
||||
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
||||
'tags': state.tagsController.getTags,
|
||||
@ -599,7 +551,7 @@ class ComposeLogic {
|
||||
);
|
||||
|
||||
// Delete draft after successful submission
|
||||
if (postType == 1) {
|
||||
if (state.postType == 1) {
|
||||
// Delete article draft
|
||||
await ref
|
||||
.read(composeStorageNotifierProvider.notifier)
|
||||
@ -642,7 +594,6 @@ class ComposeLogic {
|
||||
SnPost? originalPost,
|
||||
SnPost? repliedPost,
|
||||
SnPost? forwardedPost,
|
||||
int? postType,
|
||||
}) {
|
||||
if (event is! RawKeyDownEvent) return;
|
||||
|
||||
@ -663,7 +614,6 @@ class ComposeLogic {
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
postType: postType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -242,6 +242,47 @@ class PostItem extends HookConsumerWidget {
|
||||
? EdgeInsets.only(bottom: 8)
|
||||
: null,
|
||||
),
|
||||
// Render tags and categories if they exist
|
||||
if (item.tags.isNotEmpty || item.categories.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (item.tags.isNotEmpty)
|
||||
Wrap(
|
||||
children: [
|
||||
for (final tag in item.tags)
|
||||
InkWell(
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.label, size: 13),
|
||||
Text(tag.name ?? '#${tag.slug}')
|
||||
.fontSize(13)
|
||||
],
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.categories.isNotEmpty)
|
||||
Wrap(
|
||||
children: [
|
||||
for (final category in item.categories)
|
||||
InkWell(
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.category, size: 13),
|
||||
Text(category.name ?? '#${category.slug}')
|
||||
.fontSize(13)
|
||||
],
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// Show truncation hint if post is truncated
|
||||
if (item.isTruncated && !isFullPost)
|
||||
_PostTruncateHint().padding(
|
||||
|
@ -2301,10 +2301,11 @@ packages:
|
||||
textfield_tags:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: textfield_tags
|
||||
sha256: d1f2204114157a1296bb97c20d7f8c8c7fd036212812afb2e19de7bb34acc55b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: "fixes/allow-controller-re-registration"
|
||||
resolved-ref: "7574e79649e34df1c3cc0c49b2f0cc2b92de6a7b"
|
||||
url: "https://github.com/lionelmennig/textfield_tags.git"
|
||||
source: git
|
||||
version: "3.0.1"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
|
@ -122,7 +122,10 @@ dependencies:
|
||||
share_plus: ^11.0.0
|
||||
receive_sharing_intent: ^1.8.1
|
||||
top_snackbar_flutter: ^3.3.0
|
||||
textfield_tags: ^3.0.1
|
||||
textfield_tags:
|
||||
git:
|
||||
url: https://github.com/lionelmennig/textfield_tags.git
|
||||
ref: fixes/allow-controller-re-registration
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
x
Reference in New Issue
Block a user