Post tags

This commit is contained in:
LittleSheep 2025-06-27 02:31:21 +08:00
parent 0361f031db
commit 4deff5a920
7 changed files with 114 additions and 118 deletions

View File

@ -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,

View File

@ -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
}

View File

@ -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,

View File

@ -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,
);
}
}

View File

@ -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(

View File

@ -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"

View File

@ -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: