♻️ Refactor post tags
This commit is contained in:
@@ -1251,5 +1251,6 @@
|
|||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"availableWithYourPlan": "Available with your plan",
|
"availableWithYourPlan": "Available with your plan",
|
||||||
"upgradeRequired": "Upgrade required",
|
"upgradeRequired": "Upgrade required",
|
||||||
"settingsDisableAnimation": "Disable Animation"
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag"
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,11 @@ import 'package:dropdown_button2/dropdown_button2.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/post_category.dart';
|
import 'package:island/models/post_category.dart';
|
||||||
|
import 'package:island/models/post_tag.dart';
|
||||||
import 'package:island/models/realm.dart';
|
import 'package:island/models/realm.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/realm/realms.dart';
|
import 'package:island/screens/realm/realms.dart';
|
||||||
@@ -132,6 +134,26 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
const ComposeSettingsSheet({super.key, required this.state});
|
const ComposeSettingsSheet({super.key, required this.state});
|
||||||
|
|
||||||
|
Future<List<SnPostTag>> _fetchTagSuggestions(
|
||||||
|
String query,
|
||||||
|
WidgetRef ref,
|
||||||
|
) async {
|
||||||
|
if (query.isEmpty) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get(
|
||||||
|
'/sphere/posts/tags',
|
||||||
|
queryParameters: {'query': query},
|
||||||
|
);
|
||||||
|
return response.data
|
||||||
|
.map<SnPostTag>((json) => SnPostTag.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -140,6 +162,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
|||||||
// Listen to visibility changes to trigger rebuilds
|
// Listen to visibility changes to trigger rebuilds
|
||||||
final currentVisibility = useValueListenable(state.visibility);
|
final currentVisibility = useValueListenable(state.visibility);
|
||||||
final currentCategories = useValueListenable(state.categories);
|
final currentCategories = useValueListenable(state.categories);
|
||||||
|
final currentTags = useValueListenable(state.tags);
|
||||||
final currentRealm = useValueListenable(state.realm);
|
final currentRealm = useValueListenable(state.realm);
|
||||||
final postCategories = ref.watch(postCategoriesProvider);
|
final postCategories = ref.watch(postCategoriesProvider);
|
||||||
final userRealms = ref.watch(realmsJoinedProvider);
|
final userRealms = ref.watch(realmsJoinedProvider);
|
||||||
@@ -255,23 +278,118 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Tags field
|
// Tags field
|
||||||
TextFieldTags(
|
Container(
|
||||||
textfieldTagsController: state.tagsController,
|
decoration: BoxDecoration(
|
||||||
textSeparators: const [' ', ','],
|
border: Border.all(
|
||||||
letterCase: LetterCase.normal,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
validator: (String tag) {
|
width: 1,
|
||||||
if (tag.isEmpty) {
|
),
|
||||||
return 'No, cannot be empty';
|
borderRadius: BorderRadius.circular(12),
|
||||||
}
|
),
|
||||||
return null;
|
padding: const EdgeInsets.all(16),
|
||||||
},
|
child: Column(
|
||||||
inputFieldBuilder: (context, inputFieldValues) {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
return ChipTagInputField(
|
spacing: 12,
|
||||||
inputFieldValues: inputFieldValues,
|
children: [
|
||||||
labelText: 'tags',
|
Text(
|
||||||
hintText: 'tagsHint',
|
'tags'.tr(),
|
||||||
);
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
},
|
),
|
||||||
|
// Existing tags display
|
||||||
|
if (currentTags.isNotEmpty)
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children:
|
||||||
|
currentTags.map((tag) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'#$tag',
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
final newTags = List<String>.from(
|
||||||
|
state.tags.value,
|
||||||
|
)..remove(tag);
|
||||||
|
state.tags.value = newTags;
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 16,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
// Tag input with autocomplete
|
||||||
|
TypeAheadField<SnPostTag>(
|
||||||
|
builder: (context, controller, focusNode) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'addTag'.tr(),
|
||||||
|
border: InputBorder.none,
|
||||||
|
isCollapsed: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
state.tags.value = [...state.tags.value, value];
|
||||||
|
controller.clear();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
suggestionsCallback:
|
||||||
|
(pattern) => _fetchTagSuggestions(pattern, ref),
|
||||||
|
itemBuilder: (context, suggestion) {
|
||||||
|
return ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
title: Text('#${suggestion.slug}'),
|
||||||
|
dense: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSelected: (suggestion) {
|
||||||
|
if (!state.tags.value.contains(suggestion.slug)) {
|
||||||
|
state.tags.value = [
|
||||||
|
...state.tags.value,
|
||||||
|
suggestion.slug,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
direction: VerticalDirection.down,
|
||||||
|
hideOnEmpty: true,
|
||||||
|
hideOnLoading: true,
|
||||||
|
debounceDuration: const Duration(milliseconds: 300),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Categories field
|
// Categories field
|
||||||
|
@@ -23,7 +23,6 @@ import 'package:island/widgets/post/compose_poll.dart';
|
|||||||
import 'package:island/widgets/post/compose_recorder.dart';
|
import 'package:island/widgets/post/compose_recorder.dart';
|
||||||
import 'package:island/pods/file_pool.dart';
|
import 'package:island/pods/file_pool.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'package:textfield_tags/textfield_tags.dart';
|
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
class ComposeState {
|
class ComposeState {
|
||||||
@@ -37,7 +36,7 @@ class ComposeState {
|
|||||||
final ValueNotifier<SnPublisher?> currentPublisher;
|
final ValueNotifier<SnPublisher?> currentPublisher;
|
||||||
final ValueNotifier<bool> submitting;
|
final ValueNotifier<bool> submitting;
|
||||||
final ValueNotifier<List<SnPostCategory>> categories;
|
final ValueNotifier<List<SnPostCategory>> categories;
|
||||||
StringTagController tagsController;
|
final ValueNotifier<List<String>> tags;
|
||||||
final ValueNotifier<SnRealm?> realm;
|
final ValueNotifier<SnRealm?> realm;
|
||||||
final ValueNotifier<SnPostEmbedView?> embedView;
|
final ValueNotifier<SnPostEmbedView?> embedView;
|
||||||
final String draftId;
|
final String draftId;
|
||||||
@@ -56,7 +55,7 @@ class ComposeState {
|
|||||||
required this.attachmentProgress,
|
required this.attachmentProgress,
|
||||||
required this.currentPublisher,
|
required this.currentPublisher,
|
||||||
required this.submitting,
|
required this.submitting,
|
||||||
required this.tagsController,
|
required this.tags,
|
||||||
required this.categories,
|
required this.categories,
|
||||||
required this.realm,
|
required this.realm,
|
||||||
required this.embedView,
|
required this.embedView,
|
||||||
@@ -90,14 +89,10 @@ class ComposeLogic {
|
|||||||
int postType = 0,
|
int postType = 0,
|
||||||
}) {
|
}) {
|
||||||
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
final tagsController = StringTagController();
|
|
||||||
|
|
||||||
// Initialize tags from original post
|
// Initialize tags from original post
|
||||||
if (originalPost != null) {
|
final tags =
|
||||||
for (var tag in originalPost.tags) {
|
originalPost?.tags.map((tag) => tag.slug).toList() ?? <String>[];
|
||||||
tagsController.addTag(tag.slug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize categories from original post
|
// Initialize categories from original post
|
||||||
final categories = originalPost?.categories ?? <SnPostCategory>[];
|
final categories = originalPost?.categories ?? <SnPostCategory>[];
|
||||||
@@ -129,7 +124,7 @@ class ComposeLogic {
|
|||||||
submitting: ValueNotifier<bool>(false),
|
submitting: ValueNotifier<bool>(false),
|
||||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||||
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
||||||
tagsController: tagsController,
|
tags: ValueNotifier<List<String>>(tags),
|
||||||
categories: ValueNotifier<List<SnPostCategory>>(categories),
|
categories: ValueNotifier<List<SnPostCategory>>(categories),
|
||||||
realm: ValueNotifier(originalPost?.realm),
|
realm: ValueNotifier(originalPost?.realm),
|
||||||
embedView: ValueNotifier<SnPostEmbedView?>(originalPost?.embedView),
|
embedView: ValueNotifier<SnPostEmbedView?>(originalPost?.embedView),
|
||||||
@@ -141,10 +136,7 @@ class ComposeLogic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
|
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
|
||||||
final tagsController = StringTagController();
|
final tags = draft.tags.map((tag) => tag.slug).toList();
|
||||||
for (var x in draft.tags) {
|
|
||||||
tagsController.addTag(x.slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ComposeState(
|
return ComposeState(
|
||||||
attachments: ValueNotifier<List<UniversalFile>>(
|
attachments: ValueNotifier<List<UniversalFile>>(
|
||||||
@@ -158,7 +150,7 @@ class ComposeLogic {
|
|||||||
submitting: ValueNotifier<bool>(false),
|
submitting: ValueNotifier<bool>(false),
|
||||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||||
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
||||||
tagsController: tagsController,
|
tags: ValueNotifier<List<String>>(tags),
|
||||||
categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
|
categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
|
||||||
realm: ValueNotifier(draft.realm),
|
realm: ValueNotifier(draft.realm),
|
||||||
embedView: ValueNotifier<SnPostEmbedView?>(draft.embedView),
|
embedView: ValueNotifier<SnPostEmbedView?>(draft.embedView),
|
||||||
@@ -685,7 +677,7 @@ class ComposeLogic {
|
|||||||
'type': state.postType,
|
'type': state.postType,
|
||||||
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
||||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
||||||
'tags': state.tagsController.getTags,
|
'tags': state.tags.value,
|
||||||
'categories': state.categories.value.map((e) => e.slug).toList(),
|
'categories': state.categories.value.map((e) => e.slug).toList(),
|
||||||
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
||||||
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
||||||
@@ -781,7 +773,7 @@ class ComposeLogic {
|
|||||||
state.submitting.dispose();
|
state.submitting.dispose();
|
||||||
state.attachmentProgress.dispose();
|
state.attachmentProgress.dispose();
|
||||||
state.currentPublisher.dispose();
|
state.currentPublisher.dispose();
|
||||||
state.tagsController.dispose();
|
state.tags.dispose();
|
||||||
state.categories.dispose();
|
state.categories.dispose();
|
||||||
state.realm.dispose();
|
state.realm.dispose();
|
||||||
state.embedView.dispose();
|
state.embedView.dispose();
|
||||||
|
@@ -173,7 +173,7 @@ class ComposeStateUtils {
|
|||||||
state.attachmentProgress.value = {};
|
state.attachmentProgress.value = {};
|
||||||
|
|
||||||
// Clear tags
|
// Clear tags
|
||||||
state.tagsController.clearTags();
|
state.tags.value = [];
|
||||||
|
|
||||||
// Clear categories
|
// Clear categories
|
||||||
state.categories.value = [];
|
state.categories.value = [];
|
||||||
|
@@ -75,7 +75,7 @@ class ComposeSubmitUtils {
|
|||||||
'type': state.postType,
|
'type': state.postType,
|
||||||
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
||||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
||||||
'tags': state.tagsController.getTags,
|
'tags': state.tags.value,
|
||||||
'categories': state.categories.value.map((e) => e.slug).toList(),
|
'categories': state.categories.value.map((e) => e.slug).toList(),
|
||||||
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
||||||
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
||||||
|
Reference in New Issue
Block a user