✨ Categories and tags in subscription filter
This commit is contained in:
@@ -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;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user