From 24009dff27c6768d2ef555352056a79d1336a2b3 Mon Sep 17 00:00:00 2001 From: 21Mill Date: Mon, 17 Aug 2026 22:41:14 +0200 Subject: [PATCH 1/3] feat: show when order book filters are active The filter pill looked identical whether filters were applied or not, and the five filters persist in memory for the whole session, across BUY/SELL tab switches. A user who forgot a filter was set saw fewer offers -- or none at all -- and read it as missing liquidity rather than as their own filtering. The pill now turns Mostro green and reads FILTER (n) whenever a filter differs from its default, and carries an X that clears every filter in a single tap without opening the dialog. With no filters set it renders exactly as before. activeFilterCountProvider reuses the same predicates filteredOrdersProvider uses to decide whether each filter block applies, so the count shown can never disagree with the filtering actually performed. The default values behind those predicates were duplicated across the providers, the filtering logic and the dialog's Clear button; they are now constants, and Clear shares the new clearAllOrderFilters helper with the pill's X. Adds filterWithCount and clearFilters to the six supported locales. --- .../home/providers/home_order_providers.dart | 65 +++++- lib/features/home/screens/home_screen.dart | 186 +++++++++++++----- lib/l10n/intl_de.arb | 9 + lib/l10n/intl_en.arb | 9 + lib/l10n/intl_es.arb | 9 + lib/l10n/intl_fr.arb | 9 + lib/l10n/intl_it.arb | 9 + lib/l10n/intl_pt.arb | 9 + lib/shared/widgets/order_filter.dart | 29 ++- .../home/home_order_providers_test.dart | 124 ++++++++++++ 10 files changed, 384 insertions(+), 74 deletions(-) create mode 100644 test/features/home/home_order_providers_test.dart diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index 59116e411..ae09c4999 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -7,12 +7,63 @@ import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; final homeOrderTypeProvider = StateProvider((ref) => OrderType.sell); +// Default values for the order book filters. A filter is considered active +// only when its state differs from these defaults. +const double kDefaultRatingMin = 0.0; +const double kDefaultRatingMax = 5.0; +const double kDefaultPremiumMin = -10.0; +const double kDefaultPremiumMax = 10.0; +const int kDefaultMinDays = 0; + // Filter state providers final currencyFilterProvider = StateProvider>((ref) => []); final paymentMethodFilterProvider = StateProvider>((ref) => []); -final ratingFilterProvider = StateProvider<({double min, double max})>((ref) => (min: 0.0, max: 5.0)); -final premiumRangeFilterProvider = StateProvider<({double min, double max})>((ref) => (min: -10.0, max: 10.0)); -final minDaysFilterProvider = StateProvider((ref) => 0); +final ratingFilterProvider = StateProvider<({double min, double max})>( + (ref) => (min: kDefaultRatingMin, max: kDefaultRatingMax)); +final premiumRangeFilterProvider = StateProvider<({double min, double max})>( + (ref) => (min: kDefaultPremiumMin, max: kDefaultPremiumMax)); +final minDaysFilterProvider = StateProvider((ref) => kDefaultMinDays); + +/// Number of filters that currently differ from their default value. +/// +/// Uses the same predicates as [filteredOrdersProvider] so the indicator shown +/// in the UI can never disagree with the filtering actually applied. +final activeFilterCountProvider = Provider((ref) { + final selectedCurrencies = ref.watch(currencyFilterProvider); + final selectedPaymentMethods = ref.watch(paymentMethodFilterProvider); + final ratingRange = ref.watch(ratingFilterProvider); + final premiumRange = ref.watch(premiumRangeFilterProvider); + final minDays = ref.watch(minDaysFilterProvider); + + var count = 0; + if (selectedCurrencies.isNotEmpty) count++; + if (selectedPaymentMethods.isNotEmpty) count++; + if (minDays > kDefaultMinDays) count++; + if (ratingRange.min > kDefaultRatingMin || + ratingRange.max < kDefaultRatingMax) { + count++; + } + if (premiumRange.min > kDefaultPremiumMin || + premiumRange.max < kDefaultPremiumMax) { + count++; + } + return count; +}); + +/// Signature shared by `WidgetRef.read` and `ProviderContainer.read`, so +/// [clearAllOrderFilters] can be called from widgets and from tests alike. +typedef ProviderReader = T Function(ProviderListenable provider); + +/// Resets every order book filter to its default value. +void clearAllOrderFilters(ProviderReader read) { + read(currencyFilterProvider.notifier).state = []; + read(paymentMethodFilterProvider.notifier).state = []; + read(ratingFilterProvider.notifier).state = + (min: kDefaultRatingMin, max: kDefaultRatingMax); + read(premiumRangeFilterProvider.notifier).state = + (min: kDefaultPremiumMin, max: kDefaultPremiumMax); + read(minDaysFilterProvider.notifier).state = kDefaultMinDays; +} final filteredOrdersProvider = Provider>((ref) { final allOrdersAsync = ref.watch(orderEventsProvider); @@ -56,12 +107,13 @@ final filteredOrdersProvider = Provider>((ref) { } // Apply minimum days filter (maker's account age as reported in rating.days) - if (minDays > 0) { + if (minDays > kDefaultMinDays) { filtered = filtered.where((o) => (o.rating?.days ?? 0) >= minDays); } // Apply rating filter - if (ratingRange.min > 0.0 || ratingRange.max < 5.0) { + if (ratingRange.min > kDefaultRatingMin || + ratingRange.max < kDefaultRatingMax) { filtered = filtered.where((o) => o.rating != null && o.rating!.totalRating >= ratingRange.min && @@ -70,7 +122,8 @@ final filteredOrdersProvider = Provider>((ref) { } // Apply premium/discount filter - if (premiumRange.min > -10.0 || premiumRange.max < 10.0) { + if (premiumRange.min > kDefaultPremiumMin || + premiumRange.max < kDefaultPremiumMax) { filtered = filtered.where((o) { if (o.premium == null || o.premium!.isEmpty) return false; final premiumValue = double.tryParse(o.premium!) ?? 0.0; diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 0d8acb21d..ce2e151c1 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -197,6 +197,16 @@ class HomeScreen extends ConsumerWidget { Widget _buildFilterButton(BuildContext context, WidgetRef ref) { final filteredOrders = ref.watch(filteredOrdersProvider); + final activeFilterCount = ref.watch(activeFilterCountProvider); + final hasFilters = activeFilterCount > 0; + + // When filters are active the pill is highlighted so the user can tell at a + // glance that the offers list is being narrowed down. + final foregroundColor = + hasFilters ? AppTheme.mostroGreen : Colors.white70; + final dividerColor = hasFilters + ? AppTheme.mostroGreen.withValues(alpha: 0.4) + : Colors.white.withValues(alpha: 0.2); return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), @@ -205,9 +215,15 @@ class HomeScreen extends ConsumerWidget { alignment: Alignment.centerLeft, child: Container( decoration: BoxDecoration( - color: AppTheme.backgroundInput, + color: hasFilters + ? AppTheme.mostroGreen.withValues(alpha: 0.12) + : AppTheme.backgroundInput, borderRadius: BorderRadius.circular(30), - border: Border.all(color: Colors.white.withValues(alpha: 0.05)), + border: Border.all( + color: hasFilters + ? AppTheme.mostroGreen.withValues(alpha: 0.6) + : Colors.white.withValues(alpha: 0.05), + ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), @@ -219,65 +235,131 @@ class HomeScreen extends ConsumerWidget { child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(30), - child: InkWell( - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) { - return const Dialog( - child: OrderFilter(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const Dialog( + child: OrderFilter(), + ); + }, ); }, - ); - }, - borderRadius: BorderRadius.circular(30), - splashColor: AppTheme.activeColor.withValues(alpha: 0.3), - highlightColor: AppTheme.activeColor.withValues(alpha: 0.15), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HeroIcon( - HeroIcons.funnel, - style: HeroIconStyle.outline, - color: Colors.white70, - size: 18, - ), - const SizedBox(width: 8), - Text( - S.of(context)!.filter, - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), + borderRadius: BorderRadius.circular(30), + splashColor: AppTheme.activeColor.withValues(alpha: 0.3), + highlightColor: AppTheme.activeColor.withValues(alpha: 0.15), + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: hasFilters ? 8 : 16, + top: 12, + bottom: 12, ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - height: 16, - width: 1, - color: Colors.white.withValues(alpha: 0.2), - ), - Text( - S - .of(context)! - .offersCount(filteredOrders.length.toString()), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - fontWeight: FontWeight.normal, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + HeroIcon( + HeroIcons.funnel, + style: hasFilters + ? HeroIconStyle.solid + : HeroIconStyle.outline, + color: foregroundColor, + size: 18, + ), + const SizedBox(width: 8), + Text( + hasFilters + ? S + .of(context)! + .filterWithCount(activeFilterCount.toString()) + : S.of(context)!.filter, + style: TextStyle( + color: foregroundColor, + fontSize: 13, + fontWeight: + hasFilters ? FontWeight.w600 : FontWeight.w500, + letterSpacing: 0.5, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + height: 16, + width: 1, + color: dividerColor, + ), + Text( + S + .of(context)! + .offersCount(filteredOrders.length.toString()), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], ), - ], + ), ), - ), + if (hasFilters) + _buildClearFiltersButton(context, ref, dividerColor), + ], ), ), ), ), ); } + + /// Quick "clear all filters" action shown inside the filter pill. + /// + /// It sits outside the pill's main [InkWell] so tapping it resets the filters + /// without also opening the filter dialog. + Widget _buildClearFiltersButton( + BuildContext context, + WidgetRef ref, + Color dividerColor, + ) { + final label = S.of(context)!.clearFilters; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 16, + width: 1, + color: dividerColor, + ), + Semantics( + button: true, + label: label, + child: Tooltip( + message: label, + child: InkWell( + onTap: () => clearAllOrderFilters(ref.read), + borderRadius: BorderRadius.circular(30), + splashColor: AppTheme.mostroGreen.withValues(alpha: 0.3), + highlightColor: AppTheme.mostroGreen.withValues(alpha: 0.15), + child: const SizedBox( + width: 40, + height: 40, + child: Center( + child: HeroIcon( + HeroIcons.xMark, + style: HeroIconStyle.outline, + color: AppTheme.mostroGreen, + size: 16, + ), + ), + ), + ), + ), + ), + ], + ); + } } diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index a60934bbb..d35ef18e6 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -217,6 +217,15 @@ } } }, + "filterWithCount": "FILTER ({count})", + "@filterWithCount": { + "placeholders": { + "count": { + "type": "String" + } + } + }, + "clearFilters": "Filter zurücksetzen", "creatingNewOrder": "NEUE ORDER ERSTELLEN", "enterSatsAmountBuy": "Gib den Sats-Betrag ein, den du kaufen möchtest", "enterSatsAmountSell": "Gib den Sats-Betrag ein, den du verkaufen möchtest", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ad67dda2f..c6d7e8346 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -217,6 +217,15 @@ } } }, + "filterWithCount": "FILTER ({count})", + "@filterWithCount": { + "placeholders": { + "count": { + "type": "String" + } + } + }, + "clearFilters": "Clear filters", "creatingNewOrder": "CREATING NEW ORDER", "enterSatsAmountBuy": "Enter the Sats amount you want to Buy", "enterSatsAmountSell": "Enter the Sats amount you want to Sell", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index e8c1bcbe1..903073c04 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -181,6 +181,15 @@ } } }, + "filterWithCount": "FILTRO ({count})", + "@filterWithCount": { + "placeholders": { + "count": { + "type": "String" + } + } + }, + "clearFilters": "Limpiar filtros", "creatingNewOrder": "CREANDO NUEVA ORDEN", "enterSatsAmountBuy": "Ingresa la cantidad de Sats que quieres Comprar", "enterSatsAmountSell": "Ingresa la cantidad de Sats que quieres Vender", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 6d6f1980c..a61d24c2a 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -217,6 +217,15 @@ } } }, + "filterWithCount": "FILTRE ({count})", + "@filterWithCount": { + "placeholders": { + "count": { + "type": "String" + } + } + }, + "clearFilters": "Effacer les filtres", "creatingNewOrder": "CRÉATION D'UNE NOUVELLE COMMANDE", "enterSatsAmountBuy": "Entrez le montant de Sats que vous voulez acheter", "enterSatsAmountSell": "Entrez le montant de Sats que vous voulez vendre", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 371a1b4a2..dee5fe3e0 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -216,6 +216,15 @@ } } }, + "filterWithCount": "FILTRO ({count})", + "@filterWithCount": { + "placeholders": { + "count": { + "type": "String" + } + } + }, + "clearFilters": "Rimuovi filtri", "creatingNewOrder": "CREAZIONE NUOVO ORDINE", "enterSatsAmountBuy": "Inserisci la quantità di Sats che vuoi Comprare", "enterSatsAmountSell": "Inserisci la quantità di Sats che vuoi Vendere", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 61ef03a77..469ec6816 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -217,6 +217,15 @@ } } }, + "filterWithCount": "FILTRAR ({count})", + "@filterWithCount": { + "placeholders": { + "count": { + "type": "String" + } + } + }, + "clearFilters": "Limpar filtros", "creatingNewOrder": "CRIANDO NOVA ORDEM", "enterSatsAmountBuy": "Digite a quantidade de Sats que você quer Comprar", "enterSatsAmountSell": "Digite a quantidade de Sats que você quer Vender", diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index e9c7e85c0..84c0aab17 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -250,11 +250,11 @@ class OrderFilter extends ConsumerStatefulWidget { class OrderFilterState extends ConsumerState { List selectedFiatCurrencies = []; List selectedPaymentMethods = []; - double ratingMin = 0.0; - double ratingMax = 5.0; - double premiumMin = -10.0; - double premiumMax = 10.0; - int minDays = 0; + double ratingMin = kDefaultRatingMin; + double ratingMax = kDefaultRatingMax; + double premiumMin = kDefaultPremiumMin; + double premiumMax = kDefaultPremiumMax; + int minDays = kDefaultMinDays; final TextEditingController _daysController = TextEditingController(text: '0'); // Options for the multi-select fields. @@ -827,20 +827,17 @@ class OrderFilterState extends ConsumerState { setState(() { selectedFiatCurrencies.clear(); selectedPaymentMethods.clear(); - ratingMin = 0.0; - ratingMax = 5.0; - premiumMin = -10.0; - premiumMax = 10.0; - minDays = 0; + ratingMin = kDefaultRatingMin; + ratingMax = kDefaultRatingMax; + premiumMin = kDefaultPremiumMin; + premiumMax = kDefaultPremiumMax; + minDays = kDefaultMinDays; _daysController.text = '0'; }); - ref.read(currencyFilterProvider.notifier).state = []; - ref.read(paymentMethodFilterProvider.notifier).state = []; - ref.read(ratingFilterProvider.notifier).state = (min: 0.0, max: 5.0); - ref.read(premiumRangeFilterProvider.notifier).state = (min: -10.0, max: 10.0); - ref.read(minDaysFilterProvider.notifier).state = 0; - + clearAllOrderFilters(ref.read); + + Navigator.of(context).pop(); }, style: OutlinedButton.styleFrom( diff --git a/test/features/home/home_order_providers_test.dart b/test/features/home/home_order_providers_test.dart new file mode 100644 index 000000000..533e371c9 --- /dev/null +++ b/test/features/home/home_order_providers_test.dart @@ -0,0 +1,124 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro_mobile/features/home/providers/home_order_providers.dart'; + +void main() { + late ProviderContainer container; + + setUp(() { + container = ProviderContainer(); + }); + + tearDown(() { + container.dispose(); + }); + + int activeCount() => container.read(activeFilterCountProvider); + + group('activeFilterCountProvider', () { + test('is zero when no filter has been touched', () { + expect(activeCount(), 0); + }); + + test('counts a currency selection', () { + container.read(currencyFilterProvider.notifier).state = ['USD']; + expect(activeCount(), 1); + }); + + test('counts a payment method selection', () { + container.read(paymentMethodFilterProvider.notifier).state = ['Bank']; + expect(activeCount(), 1); + }); + + test('counts a minimum days filter above the default', () { + container.read(minDaysFilterProvider.notifier).state = 5; + expect(activeCount(), 1); + }); + + test('does not count minimum days left at the default', () { + container.read(minDaysFilterProvider.notifier).state = kDefaultMinDays; + expect(activeCount(), 0); + }); + + test('counts a rating range narrowed only on the lower bound', () { + container.read(ratingFilterProvider.notifier).state = + (min: 3.0, max: kDefaultRatingMax); + expect(activeCount(), 1); + }); + + test('counts a rating range narrowed only on the upper bound', () { + container.read(ratingFilterProvider.notifier).state = + (min: kDefaultRatingMin, max: 4.0); + expect(activeCount(), 1); + }); + + test('stops counting a rating range restored to its defaults', () { + final notifier = container.read(ratingFilterProvider.notifier); + notifier.state = (min: 3.0, max: 4.0); + expect(activeCount(), 1); + + notifier.state = (min: kDefaultRatingMin, max: kDefaultRatingMax); + expect(activeCount(), 0); + }); + + test('counts a premium range narrowed only on the lower bound', () { + container.read(premiumRangeFilterProvider.notifier).state = + (min: -5.0, max: kDefaultPremiumMax); + expect(activeCount(), 1); + }); + + test('counts a premium range narrowed only on the upper bound', () { + container.read(premiumRangeFilterProvider.notifier).state = + (min: kDefaultPremiumMin, max: 5.0); + expect(activeCount(), 1); + }); + + test('stops counting a premium range restored to its defaults', () { + final notifier = container.read(premiumRangeFilterProvider.notifier); + notifier.state = (min: -5.0, max: 5.0); + expect(activeCount(), 1); + + notifier.state = (min: kDefaultPremiumMin, max: kDefaultPremiumMax); + expect(activeCount(), 0); + }); + + test('adds up every active filter', () { + container.read(currencyFilterProvider.notifier).state = ['USD', 'EUR']; + container.read(paymentMethodFilterProvider.notifier).state = ['Bank']; + container.read(minDaysFilterProvider.notifier).state = 10; + container.read(ratingFilterProvider.notifier).state = (min: 3.0, max: 5.0); + container.read(premiumRangeFilterProvider.notifier).state = + (min: -2.0, max: 2.0); + + expect(activeCount(), 5); + }); + }); + + group('clearAllOrderFilters', () { + test('restores every filter to its default value', () { + container.read(currencyFilterProvider.notifier).state = ['USD']; + container.read(paymentMethodFilterProvider.notifier).state = ['Bank']; + container.read(minDaysFilterProvider.notifier).state = 10; + container.read(ratingFilterProvider.notifier).state = (min: 3.0, max: 4.0); + container.read(premiumRangeFilterProvider.notifier).state = + (min: -2.0, max: 2.0); + expect(activeCount(), 5); + + clearAllOrderFilters(container.read); + + expect(activeCount(), 0); + expect(container.read(currencyFilterProvider), isEmpty); + expect(container.read(paymentMethodFilterProvider), isEmpty); + expect(container.read(minDaysFilterProvider), kDefaultMinDays); + expect(container.read(ratingFilterProvider), + (min: kDefaultRatingMin, max: kDefaultRatingMax)); + expect(container.read(premiumRangeFilterProvider), + (min: kDefaultPremiumMin, max: kDefaultPremiumMax)); + }); + + test('is a no-op when no filter is active', () { + clearAllOrderFilters(container.read); + expect(activeCount(), 0); + }); + }); +} From 80e36027e7ff1514845b40d72c4868ec78317356 Mon Sep 17 00:00:00 2001 From: 21Mill Date: Tue, 18 Aug 2026 21:33:15 +0200 Subject: [PATCH 2/3] refactor: derive filter activity from a single predicate The conditions that decide whether each filter is active were written out twice: once in activeFilterCountProvider to count them, once in filteredOrdersProvider to apply them. A comment asked future edits to keep the two in step, but nothing enforced it -- an edit to one would diverge silently, and the first sign would be a pill claiming filters that the list is not applying. They now go through _isMinDaysActive, _isRatingActive and _isPremiumActive, so counting and filtering read the same definition. The premium and rating RangeSliders also went back to hardcoded bounds while the fields feeding them initialize from the shared constants. The values match today, so there is no bug; if a constant moved without the slider following, initState would seed a value outside the slider's range and trip a RangeSlider assertion. Both now take their bounds from the constants. The days Slider keeps its literals: its 20 has no constant, and the value it receives is clamped. --- .../home/providers/home_order_providers.dart | 33 +++++++++---------- lib/shared/widgets/order_filter.dart | 8 ++--- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index ae09c4999..09938d2cb 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -24,10 +24,17 @@ final premiumRangeFilterProvider = StateProvider<({double min, double max})>( (ref) => (min: kDefaultPremiumMin, max: kDefaultPremiumMax)); final minDaysFilterProvider = StateProvider((ref) => kDefaultMinDays); +/// Single definition of "this filter is active", so the count shown in the UI +/// can never disagree with the filtering actually applied. +bool _isMinDaysActive(int minDays) => minDays > kDefaultMinDays; + +bool _isRatingActive(({double min, double max}) range) => + range.min > kDefaultRatingMin || range.max < kDefaultRatingMax; + +bool _isPremiumActive(({double min, double max}) range) => + range.min > kDefaultPremiumMin || range.max < kDefaultPremiumMax; + /// Number of filters that currently differ from their default value. -/// -/// Uses the same predicates as [filteredOrdersProvider] so the indicator shown -/// in the UI can never disagree with the filtering actually applied. final activeFilterCountProvider = Provider((ref) { final selectedCurrencies = ref.watch(currencyFilterProvider); final selectedPaymentMethods = ref.watch(paymentMethodFilterProvider); @@ -38,15 +45,9 @@ final activeFilterCountProvider = Provider((ref) { var count = 0; if (selectedCurrencies.isNotEmpty) count++; if (selectedPaymentMethods.isNotEmpty) count++; - if (minDays > kDefaultMinDays) count++; - if (ratingRange.min > kDefaultRatingMin || - ratingRange.max < kDefaultRatingMax) { - count++; - } - if (premiumRange.min > kDefaultPremiumMin || - premiumRange.max < kDefaultPremiumMax) { - count++; - } + if (_isMinDaysActive(minDays)) count++; + if (_isRatingActive(ratingRange)) count++; + if (_isPremiumActive(premiumRange)) count++; return count; }); @@ -107,13 +108,12 @@ final filteredOrdersProvider = Provider>((ref) { } // Apply minimum days filter (maker's account age as reported in rating.days) - if (minDays > kDefaultMinDays) { + if (_isMinDaysActive(minDays)) { filtered = filtered.where((o) => (o.rating?.days ?? 0) >= minDays); } // Apply rating filter - if (ratingRange.min > kDefaultRatingMin || - ratingRange.max < kDefaultRatingMax) { + if (_isRatingActive(ratingRange)) { filtered = filtered.where((o) => o.rating != null && o.rating!.totalRating >= ratingRange.min && @@ -122,8 +122,7 @@ final filteredOrdersProvider = Provider>((ref) { } // Apply premium/discount filter - if (premiumRange.min > kDefaultPremiumMin || - premiumRange.max < kDefaultPremiumMax) { + if (_isPremiumActive(premiumRange)) { filtered = filtered.where((o) { if (o.premium == null || o.premium!.isEmpty) return false; final premiumValue = double.tryParse(o.premium!) ?? 0.0; diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index 84c0aab17..8b6663dbd 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -571,8 +571,8 @@ class OrderFilterState extends ConsumerState { ), child: RangeSlider( values: RangeValues(premiumMin, premiumMax), - min: -10.0, - max: 10.0, + min: kDefaultPremiumMin, + max: kDefaultPremiumMax, divisions: 20, labels: RangeLabels( "${premiumMin.toInt()}%", @@ -651,8 +651,8 @@ class OrderFilterState extends ConsumerState { ), child: RangeSlider( values: RangeValues(ratingMin, ratingMax), - min: 0.0, - max: 5.0, + min: kDefaultRatingMin, + max: kDefaultRatingMax, divisions: 5, labels: RangeLabels( ratingMin.toInt().toString(), From 768a858a32820e5570f5fc0c9f919da706ce951e Mon Sep 17 00:00:00 2001 From: 21Mill Date: Tue, 18 Aug 2026 21:44:06 +0200 Subject: [PATCH 3/3] fix: reset the days field to the shared default The Clear button set minDays to kDefaultMinDays but wrote '0' into the text field, so a change to the constant would leave the field showing one value and the provider holding another. The controller's initial text and the fallback for unparseable input carried the same literal. --- lib/shared/widgets/order_filter.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/shared/widgets/order_filter.dart b/lib/shared/widgets/order_filter.dart index 8b6663dbd..d08d266b9 100644 --- a/lib/shared/widgets/order_filter.dart +++ b/lib/shared/widgets/order_filter.dart @@ -255,7 +255,8 @@ class OrderFilterState extends ConsumerState { double premiumMin = kDefaultPremiumMin; double premiumMax = kDefaultPremiumMax; int minDays = kDefaultMinDays; - final TextEditingController _daysController = TextEditingController(text: '0'); + final TextEditingController _daysController = + TextEditingController(text: kDefaultMinDays.toString()); // Options for the multi-select fields. @@ -800,7 +801,9 @@ class OrderFilterState extends ConsumerState { onChanged: (text) { final parsed = int.tryParse(text); setState(() { - minDays = parsed == null ? 0 : parsed.clamp(0, 9999); + minDays = parsed == null + ? kDefaultMinDays + : parsed.clamp(0, 9999); }); }, ), @@ -832,7 +835,7 @@ class OrderFilterState extends ConsumerState { premiumMin = kDefaultPremiumMin; premiumMax = kDefaultPremiumMax; minDays = kDefaultMinDays; - _daysController.text = '0'; + _daysController.text = kDefaultMinDays.toString(); }); clearAllOrderFilters(ref.read);