Explore subscription filter card

This commit is contained in:
2025-12-23 01:03:46 +08:00
parent 0a179acb13
commit d94baab877
6 changed files with 299 additions and 205 deletions

View File

@@ -3,153 +3,129 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/post/post_subscriptions.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
final subscriptionsProvider = FutureProvider<List<SnPublisherSubscription>>((
ref,
) async {
final client = ref.read(apiClientProvider);
final response = await client.get('/sphere/publishers/subscriptions');
return response.data
.map((json) => SnPublisherSubscription.fromJson(json))
.cast<SnPublisherSubscription>()
.toList();
});
class PostSubscriptionFilterWidget extends HookConsumerWidget {
final List<String> initialSelectedPublisherIds;
final List<String> initialSelectedPublisherNames;
final ValueChanged<List<String>> onSelectedPublishersChanged;
final bool hideSearch;
const PostSubscriptionFilterWidget({
super.key,
required this.initialSelectedPublisherIds,
required this.initialSelectedPublisherNames,
required this.onSelectedPublishersChanged,
this.hideSearch = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedPublisherIds = useState<List<String>>(
initialSelectedPublisherIds,
final selectedPublisherNames = useState<List<String>>(
initialSelectedPublisherNames,
);
final showSubscriptions = useState<bool>(false);
final subscriptionsAsync = ref.watch(subscriptionsProvider);
void updateSelection() {
onSelectedPublishersChanged(selectedPublisherIds.value);
onSelectedPublishersChanged(selectedPublisherNames.value);
}
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ListTile(
title: Text('filterBySubscriptions'.tr()),
leading: const Icon(Symbols.subscriptions),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(const Radius.circular(8)),
),
trailing: Icon(
showSubscriptions.value
? Symbols.expand_less
: Symbols.expand_more,
),
onTap: () {
showSubscriptions.value = !showSubscriptions.value;
},
),
if (showSubscriptions.value) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
subscriptionsAsync.when(
data: (subscriptions) {
if (subscriptions.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('noSubscriptions'.tr()),
),
);
}
return Column(
children: subscriptions.map((subscription) {
final isSelected = selectedPublisherIds.value
.contains(subscription.publisherId);
final publisher = subscription.publisher;
return CheckboxListTile(
title: Text(publisher.name),
subtitle:
publisher.nick.isNotEmpty &&
publisher.nick != publisher.name
? Text(publisher.nick)
: null,
value: isSelected,
onChanged: (value) {
if (value == true) {
selectedPublisherIds.value = [
...selectedPublisherIds.value,
subscription.publisherId,
];
} else {
selectedPublisherIds.value =
selectedPublisherIds.value
.where(
(id) =>
id != subscription.publisherId,
)
.toList();
}
updateSelection();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.person),
);
}).toList(),
);
},
loading: () => const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
),
error: (error, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('errorLoadingSubscriptions'.tr()),
),
),
Row(
spacing: 16,
children: [
const Icon(Symbols.subscriptions, size: 20),
Text(
'exploreFilterSubscriptions'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
],
).padding(horizontal: 16, top: 12),
const Gap(12),
subscriptionsAsync.when(
data: (subscriptions) {
if (subscriptions.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('noSubscriptions'.tr()),
),
if (subscriptionsAsync.hasValue &&
subscriptionsAsync.value!.isNotEmpty) ...[
const Gap(12),
Row(
children: [
TextButton(
onPressed: () {
selectedPublisherIds.value = subscriptionsAsync
.value!
.map((s) => s.publisherId)
.toList();
updateSelection();
},
child: Text('selectAll'.tr()),
),
const Gap(8),
TextButton(
onPressed: () {
selectedPublisherIds.value = [];
updateSelection();
},
child: Text('selectNone'.tr()),
),
],
);
}
return Column(
children: subscriptions.map((subscription) {
final isSelected = selectedPublisherNames.value.contains(
subscription.publisher.name,
);
final publisher = subscription.publisher;
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
],
],
value: isSelected,
onChanged: (value) {
if (value == true) {
selectedPublisherNames.value = [
...selectedPublisherNames.value,
subscription.publisher.name,
];
} else {
selectedPublisherNames.value = selectedPublisherNames
.value
.where(
(name) => name != subscription.publisher.name,
)
.toList();
}
updateSelection();
},
dense: true,
secondary: ProfilePictureWidget(
file: subscription.publisher.picture,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
);
}).toList(),
);
},
loading: () => const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
),
],
error: (error, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('errorLoadingSubscriptions'.tr()),
),
),
),
],
),
);