Categories and tags in subscription filter

This commit is contained in:
2025-12-24 00:45:35 +08:00
parent ac82fdb8c8
commit 689965c582
2 changed files with 186 additions and 52 deletions

View File

@@ -42,6 +42,8 @@ class ExploreScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentFilter = useState<String?>(null); final currentFilter = useState<String?>(null);
final selectedPublisherNames = useState<List<String>>([]); final selectedPublisherNames = useState<List<String>>([]);
final selectedCategoryIds = useState<List<String>>([]);
final selectedTagIds = useState<List<String>>([]);
final notifier = ref.watch(activityListProvider.notifier); final notifier = ref.watch(activityListProvider.notifier);
useEffect(() { useEffect(() {
@@ -240,6 +242,8 @@ class ExploreScreen extends HookConsumerWidget {
selectedDay, selectedDay,
currentFilter.value, currentFilter.value,
selectedPublisherNames, selectedPublisherNames,
selectedCategoryIds,
selectedTagIds,
) )
: _buildNarrowBody(context, ref, currentFilter.value), : _buildNarrowBody(context, ref, currentFilter.value),
), ),
@@ -295,13 +299,19 @@ class ExploreScreen extends HookConsumerWidget {
Widget _buildPostList( Widget _buildPostList(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
List<String> selectedPublisherIds, List<String> selectedPublishers,
List<String> selectedCategories,
List<String> selectedTags,
) { ) {
return SliverPostList( return SliverPostList(
queryKey: 'explore_filtered', queryKey: 'explore_filtered',
query: PostListQuery(publishers: selectedPublisherIds), query: PostListQuery(
publishers: selectedPublishers,
categories: selectedCategories,
tags: selectedTags,
),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemPadding: EdgeInsets.zero, itemPadding: const EdgeInsets.only(bottom: 8),
); );
} }
@@ -315,12 +325,23 @@ class ExploreScreen extends HookConsumerWidget {
AsyncValue<List<dynamic>> events, AsyncValue<List<dynamic>> events,
ValueNotifier<DateTime> selectedDay, ValueNotifier<DateTime> selectedDay,
String? currentFilter, String? currentFilter,
ValueNotifier<List<String>> selectedPublisherNames, ValueNotifier<List<String>> selectedPublishers,
ValueNotifier<List<String>> selectedCategories,
ValueNotifier<List<String>> selectedTags,
) { ) {
// Use post list when subscription filter is active and publishers are selected // Use post list when subscription filter is active and publishers are selected
final usePostList = selectedPublisherNames.value.isNotEmpty; final usePostList =
selectedPublishers.value.isNotEmpty ||
selectedCategories.value.isNotEmpty ||
selectedTags.value.isNotEmpty;
final bodyView = usePostList final bodyView = usePostList
? _buildPostList(context, ref, selectedPublisherNames.value) ? _buildPostList(
context,
ref,
selectedPublishers.value,
selectedCategories.value,
selectedTags.value,
)
: _buildActivityList(context, ref); : _buildActivityList(context, ref);
final notifier = usePostList final notifier = usePostList
@@ -357,10 +378,17 @@ class ExploreScreen extends HookConsumerWidget {
children: [ children: [
Gap(4 + MediaQuery.paddingOf(context).top), Gap(4 + MediaQuery.paddingOf(context).top),
PostSubscriptionFilterWidget( PostSubscriptionFilterWidget(
initialSelectedPublisherNames: initialSelectedPublishers: selectedPublishers.value,
selectedPublisherNames.value, initialSelectedCategories: selectedCategories.value,
initialSelectedTags: selectedTags.value,
onSelectedPublishersChanged: (names) { onSelectedPublishersChanged: (names) {
selectedPublisherNames.value = names; selectedPublishers.value = names;
},
onSelectedCategoriesChanged: (ids) {
selectedCategories.value = ids;
},
onSelectedTagsChanged: (ids) {
selectedTags.value = ids;
}, },
), ),
], ],

View File

@@ -38,27 +38,42 @@ Future<List<SnCategorySubscription>> categoriesSubscriptions(Ref ref) async {
} }
class PostSubscriptionFilterWidget extends HookConsumerWidget { class PostSubscriptionFilterWidget extends HookConsumerWidget {
final List<String> initialSelectedPublisherNames; final List<String> initialSelectedPublishers;
final List<String> initialSelectedCategories;
final List<String> initialSelectedTags;
final ValueChanged<List<String>> onSelectedPublishersChanged; final ValueChanged<List<String>> onSelectedPublishersChanged;
final ValueChanged<List<String>> onSelectedCategoriesChanged;
final ValueChanged<List<String>> onSelectedTagsChanged;
final bool hideSearch; final bool hideSearch;
const PostSubscriptionFilterWidget({ const PostSubscriptionFilterWidget({
super.key, super.key,
required this.initialSelectedPublisherNames, required this.initialSelectedPublishers,
required this.initialSelectedCategories,
required this.initialSelectedTags,
required this.onSelectedPublishersChanged, required this.onSelectedPublishersChanged,
required this.onSelectedCategoriesChanged,
required this.onSelectedTagsChanged,
this.hideSearch = false, this.hideSearch = false,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final selectedPublisherNames = useState<List<String>>( final selectedPublishers = useState<List<String>>(
initialSelectedPublisherNames, initialSelectedPublishers,
); );
final selectedCategories = useState<List<String>>(
initialSelectedCategories,
);
final selectedTags = useState<List<String>>(initialSelectedTags);
final subscriptionsAsync = ref.watch(publishersSubscriptionsProvider); final publishersAsync = ref.watch(publishersSubscriptionsProvider);
final categoriesAsync = ref.watch(categoriesSubscriptionsProvider);
void updateSelection() { void updateSelection() {
onSelectedPublishersChanged(selectedPublisherNames.value); onSelectedPublishersChanged(selectedPublishers.value);
onSelectedCategoriesChanged(selectedCategories.value);
onSelectedTagsChanged(selectedTags.value);
} }
return Card( return Card(
@@ -77,7 +92,9 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget {
], ],
).padding(horizontal: 16, top: 12), ).padding(horizontal: 16, top: 12),
const Gap(12), const Gap(12),
subscriptionsAsync.when(
// Publishers Section
publishersAsync.when(
data: (subscriptions) { data: (subscriptions) {
if (subscriptions.isEmpty) { if (subscriptions.isEmpty) {
return Center( return Center(
@@ -89,43 +106,53 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget {
} }
return Column( return Column(
children: subscriptions.map((subscription) { crossAxisAlignment: CrossAxisAlignment.stretch,
final isSelected = selectedPublisherNames.value.contains( children: [
subscription.publisher.name, Text(
); 'publishers'.tr(),
final publisher = subscription.publisher; style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
).padding(bottom: 8, horizontal: 16),
...subscriptions.map((subscription) {
final isSelected = selectedPublishers.value.contains(
subscription.publisher.name,
);
final publisher = subscription.publisher;
return CheckboxListTile( return CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing, controlAffinity: ListTileControlAffinity.trailing,
title: Text(publisher.nick), title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'), subtitle: Text('@${publisher.name}'),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
value: isSelected, value: isSelected,
onChanged: (value) { onChanged: (value) {
if (value == true) { if (value == true) {
selectedPublisherNames.value = [ selectedPublishers.value = [
...selectedPublisherNames.value, ...selectedPublishers.value,
subscription.publisher.name, subscription.publisher.name,
]; ];
} else { } else {
selectedPublisherNames.value = selectedPublisherNames selectedPublishers.value = selectedPublishers.value
.value .where(
.where( (name) => name != subscription.publisher.name,
(name) => name != subscription.publisher.name, )
) .toList();
.toList(); }
} updateSelection();
updateSelection(); },
}, dense: true,
dense: true, secondary: ProfilePictureWidget(
secondary: ProfilePictureWidget( file: subscription.publisher.picture,
file: subscription.publisher.picture, ),
), contentPadding: const EdgeInsets.symmetric(
contentPadding: const EdgeInsets.symmetric(horizontal: 16), horizontal: 16,
); ),
}).toList(), );
}),
],
); );
}, },
loading: () => const Center( loading: () => const Center(
@@ -141,6 +168,85 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget {
), ),
), ),
), ),
const Divider(height: 1).padding(vertical: 8),
// Categories Section
categoriesAsync.when(
data: (subscriptions) {
if (subscriptions.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'categoriesAndTags'.tr(),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
).padding(bottom: 8, horizontal: 16),
...subscriptions.map((subscription) {
final category = subscription.category;
final tag = subscription.tag;
final slug = category?.slug ?? tag?.slug;
final displayTitle =
category?.categoryDisplayTitle ??
tag?.name ??
slug ??
'';
final isCategorySelected = selectedCategories.value
.contains(slug);
final isTagSelected = selectedTags.value.contains(slug);
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(displayTitle),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
secondary: category != null
? Icon(Symbols.category)
: Icon(Symbols.tag),
value: category != null
? isCategorySelected
: isTagSelected,
onChanged: (value) {
if (value == true) {
if (category != null) {
selectedCategories.value = [
...selectedCategories.value,
slug!,
];
} else if (tag != null) {
selectedTags.value = [...selectedTags.value, slug!];
}
} else {
if (category != null) {
selectedCategories.value = selectedCategories.value
.where((id) => id != slug)
.toList();
} else if (tag != null) {
selectedTags.value = selectedTags.value
.where((id) => id != slug)
.toList();
}
}
updateSelection();
},
dense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
);
}).toList(),
],
);
},
loading: () => const SizedBox.shrink(),
error: (error, stack) => const SizedBox.shrink(),
),
], ],
), ),
); );