💄 Redesign the post tags and categories page

This commit is contained in:
2025-12-06 20:40:28 +08:00
parent 36b0f55a47
commit 3ef2f13dd3
8 changed files with 348 additions and 392 deletions

View File

@@ -9,33 +9,14 @@ import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/post/post_categories.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'compose_settings_sheet.g.dart';
@riverpod
Future<List<SnPostCategory>> postCategories(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/categories');
final categories =
resp.data
.map((e) => SnPostCategory.fromJson(e))
.cast<SnPostCategory>()
.toList();
// Remove duplicates based on id
final uniqueCategories = <String, SnPostCategory>{};
for (final category in categories) {
uniqueCategories[category.id] = category;
}
return uniqueCategories.values.toList();
}
class ComposeSettingsSheet extends HookConsumerWidget {
final ComposeState state;
@@ -121,39 +102,38 @@ class ComposeSettingsSheet extends HookConsumerWidget {
void showVisibilitySheet() {
showModalBottomSheet(
context: context,
builder:
(context) => SheetScaffold(
titleText: 'postVisibility'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
buildVisibilityOption(
context,
0,
Symbols.public,
'postVisibilityPublic',
),
buildVisibilityOption(
context,
1,
Symbols.group,
'postVisibilityFriends',
),
buildVisibilityOption(
context,
2,
Symbols.link_off,
'postVisibilityUnlisted',
),
buildVisibilityOption(
context,
3,
Symbols.lock,
'postVisibilityPrivate',
),
],
builder: (context) => SheetScaffold(
titleText: 'postVisibility'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
buildVisibilityOption(
context,
0,
Symbols.public,
'postVisibilityPublic',
),
),
buildVisibilityOption(
context,
1,
Symbols.group,
'postVisibilityFriends',
),
buildVisibilityOption(
context,
2,
Symbols.link_off,
'postVisibilityUnlisted',
),
buildVisibilityOption(
context,
3,
Symbols.lock,
'postVisibilityPrivate',
),
],
),
),
);
}
@@ -182,8 +162,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Tags field
@@ -209,51 +189,48 @@ class ComposeSettingsSheet extends HookConsumerWidget {
Wrap(
spacing: 8,
runSpacing: 8,
children:
currentTags.map((tag) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(16),
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,
),
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
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,
),
),
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(),
],
),
);
}).toList(),
),
// Tag input with autocomplete
TypeAheadField<SnPostTag>(
@@ -274,8 +251,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
},
);
},
suggestionsCallback:
(pattern) => _fetchTagSuggestions(pattern, ref),
suggestionsCallback: (pattern) =>
_fetchTagSuggestions(pattern, ref),
itemBuilder: (context, suggestion) {
return ListTile(
shape: const RoundedRectangleBorder(
@@ -314,55 +291,49 @@ class ComposeSettingsSheet extends HookConsumerWidget {
),
),
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
items:
(postCategories.value ?? <SnPostCategory>[]).map((item) {
return DropdownMenuItem(
value: item,
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final isSelected = state.categories.value.contains(
item,
);
return InkWell(
onTap: () {
isSelected
? state.categories.value =
state.categories.value
.where((e) => e != item)
.toList()
: state.categories.value = [
...state.categories.value,
item,
];
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
item.categoryDisplayTitle,
style: const TextStyle(fontSize: 14),
),
),
],
),
),
);
items: (postCategories.value ?? <SnPostCategory>[]).map((item) {
return DropdownMenuItem(
value: item,
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final isSelected = state.categories.value.contains(item);
return InkWell(
onTap: () {
isSelected
? state.categories.value = state.categories.value
.where((e) => e != item)
.toList()
: state.categories.value = [
...state.categories.value,
item,
];
menuSetState(() {});
},
),
);
}).toList(),
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
item.categoryDisplayTitle,
style: const TextStyle(fontSize: 14),
),
),
],
),
),
);
},
),
);
}).toList(),
value: currentCategories.isEmpty ? null : currentCategories.last,
onChanged: (_) {},
selectedItemBuilder: (context) {

View File

@@ -1,51 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'compose_settings_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(postCategories)
const postCategoriesProvider = PostCategoriesProvider._();
final class PostCategoriesProvider
extends
$FunctionalProvider<
AsyncValue<List<SnPostCategory>>,
List<SnPostCategory>,
FutureOr<List<SnPostCategory>>
>
with
$FutureModifier<List<SnPostCategory>>,
$FutureProvider<List<SnPostCategory>> {
const PostCategoriesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'postCategoriesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$postCategoriesHash();
@$internal
@override
$FutureProviderElement<List<SnPostCategory>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnPostCategory>> create(Ref ref) {
return postCategories(ref);
}
}
String _$postCategoriesHash() => r'8799c10eb91cf8c8c7ea72eff3475e1eaa7b9a2b';