From 1aadd9c3684f867fc5155ca2b6e8c4a35c62ccc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 9 Nov 2025 22:51:37 +0100 Subject: [PATCH] Add filter to subscription-list --- flutter/lib/api/api_client.dart | 22 +++++++-- .../components/filter_chips/filter_chips.dart | 47 +++++++++++++++++++ flutter/lib/pages/account/account.dart | 4 +- .../subscription_list/subscription_list.dart | 44 ++++++++++++++++- 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 flutter/lib/components/filter_chips/filter_chips.dart diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 25344d7..4b1a0f6 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -54,6 +54,20 @@ class MessageFilter { }); } +class SubscriptionFilter { + static final SubscriptionFilter ALL = SubscriptionFilter('both', 'all', 'all'); + static final SubscriptionFilter OWNED_INACTIVE = SubscriptionFilter('outgoing', 'unconfirmed', 'false'); + static final SubscriptionFilter OWNED_ACTIVE = SubscriptionFilter('outgoing', 'confirmed', 'false'); + static final SubscriptionFilter EXTERNAL_ALL = SubscriptionFilter('outgoing', 'all', 'true'); + static final SubscriptionFilter INCOMING_ALL = SubscriptionFilter('incoming', 'all', 'true'); + + final String direction; // 'outgoing' | 'incoming' | 'both' + final String confirmation; // 'confirmed' | 'unconfirmed' | 'all' + final String external; // 'true' | 'false' | 'all' + + SubscriptionFilter(this.direction, this.confirmation, this.external) {} +} + class APIClient { static const String _base = 'https://simplecloudnotifier.de'; static const String _prefix = '/api/v2'; @@ -345,15 +359,15 @@ class APIClient { ); } - static Future> getSubscriptionList(TokenSource auth) async { + static Future> getSubscriptionList(TokenSource auth, SubscriptionFilter filter) async { return await _request( name: 'getSubscriptionList', method: 'GET', relURL: 'users/${auth.getUserID()}/subscriptions', query: { - 'direction': ['both'], - 'confirmation': ['all'], - 'external': ['all'], + 'direction': [filter.direction], + 'confirmation': [filter.confirmation], + 'external': [filter.external], }, fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List), authToken: auth.getToken(), diff --git a/flutter/lib/components/filter_chips/filter_chips.dart b/flutter/lib/components/filter_chips/filter_chips.dart new file mode 100644 index 0000000..681c53b --- /dev/null +++ b/flutter/lib/components/filter_chips/filter_chips.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class FilterChips extends StatelessWidget { + final List<(T, String)> options; + final T value; + final void Function(T)? onChanged; + + const FilterChips({ + Key? key, + required this.options, + required this.value, + required this.onChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var opt in options) _buildChiplet(context, opt.$1, opt.$2), + ], + ), + ); + } + + Widget _buildChiplet(BuildContext context, T optValue, String optText) { + final isSelected = optValue == value; + return Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 4, 2), + child: InputChip( + label: Text( + optText, + style: isSelected ? TextStyle(color: Theme.of(context).colorScheme.onPrimary) : null, + ), + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + isEnabled: true, + selected: true, + showCheckmark: false, + onPressed: () { + if (!isSelected) onChanged?.call(optValue); + }, + selectedColor: isSelected ? Theme.of(context).colorScheme.primary : null, + ), + ); + } +} diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart index 7d287ac..583c5d3 100644 --- a/flutter/lib/pages/account/account.dart +++ b/flutter/lib/pages/account/account.dart @@ -117,7 +117,7 @@ class _AccountRootPageState extends State { _futureSubscriptionCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); - final subs = await APIClient.getSubscriptionList(userAcc); + final subs = await APIClient.getSubscriptionList(userAcc, SubscriptionFilter.ALL); return subs.length; }()); @@ -153,7 +153,7 @@ class _AccountRootPageState extends State { // refresh all data and then replace teh futures used in build() final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all); - final subs = await APIClient.getSubscriptionList(userAcc); + final subs = await APIClient.getSubscriptionList(userAcc, SubscriptionFilter.ALL); final clients = await APIClient.getClientList(userAcc); final keys = await APIClient.getKeyTokenList(userAcc); final senderNames = await APIClient.getSenderNameList(userAcc); diff --git a/flutter/lib/pages/subscription_list/subscription_list.dart b/flutter/lib/pages/subscription_list/subscription_list.dart index d37d622..39d7e31 100644 --- a/flutter/lib/pages/subscription_list/subscription_list.dart +++ b/flutter/lib/pages/subscription_list/subscription_list.dart @@ -3,6 +3,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; +import 'package:simplecloudnotifier/components/filter_chips/filter_chips.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/subscription.dart'; @@ -13,6 +14,29 @@ import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart'; import 'package:simplecloudnotifier/state/scn_data_cache.dart'; +enum SubscriptionListFilter { + ALL, + INACTIVE, + OWN, + EXTERNAL, + INCOMING; + + SubscriptionFilter toAPIFilter() { + switch (this) { + case ALL: + return SubscriptionFilter.ALL; + case INACTIVE: + return SubscriptionFilter.OWNED_INACTIVE; + case OWN: + return SubscriptionFilter.OWNED_ACTIVE; + case EXTERNAL: + return SubscriptionFilter.EXTERNAL_ALL; + case INCOMING: + return SubscriptionFilter.INCOMING_ALL; + } + } +} + class SubscriptionListPage extends StatefulWidget { const SubscriptionListPage({super.key}); @@ -26,6 +50,8 @@ class _SubscriptionListPageState extends State { final userCache = Map(); final channelCache = Map(); + SubscriptionListFilter filter = SubscriptionListFilter.ALL; + @override void initState() { super.initState(); @@ -60,7 +86,7 @@ class _SubscriptionListPageState extends State { } try { - final items = (await APIClient.getSubscriptionList(acc)).toList(); + final items = (await APIClient.getSubscriptionList(acc, filter.toAPIFilter())).toList(); items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated)); @@ -106,6 +132,22 @@ class _SubscriptionListPageState extends State { extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), hidden: !AppSettings().showInfoAlerts, ), + FilterChips( + options: [ + (SubscriptionListFilter.ALL, 'All'), + (SubscriptionListFilter.OWN, 'Own'), + (SubscriptionListFilter.EXTERNAL, 'External'), + (SubscriptionListFilter.INCOMING, 'Incoming'), + (SubscriptionListFilter.INACTIVE, 'Inactive'), + ], + value: filter, + onChanged: (newFilter) { + setState(() { + filter = newFilter; + _pagingController.refresh(); + }); + }, + ), Expanded( child: _buildList(context), )