From 49c7ef28aeb9f8384ba5264b111ad6a36fd25372 Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Thu, 23 Jan 2025 22:20:32 +0100 Subject: [PATCH] feat: similar station button, auto data safe mode, move handlers --- lib/app/app_model.dart | 12 ++++ lib/app/connectivity_model.dart | 37 +++++++++- lib/app/view/desktop_home_page.dart | 82 +++------------------- lib/app/view/discord_connect_content.dart | 35 +++++++++ lib/app/view/mobile_page.dart | 69 ++---------------- lib/common/view/bandwidth_dialog.dart | 37 ---------- lib/common/view/data_safe_mode_dialog.dart | 44 ++++++++++++ lib/extensions/connectivity_x.dart | 2 +- lib/l10n/app_en.arb | 2 + lib/player/player_service.dart | 2 +- lib/player/view/player_main_controls.dart | 8 +-- lib/podcasts/download_model.dart | 8 +++ lib/podcasts/podcast_model.dart | 28 ++++++++ lib/radio/view/next_station_button.dart | 3 +- lib/search/search_model.dart | 73 +++++++++---------- needs_translation.json | 40 +++++++++++ 16 files changed, 262 insertions(+), 220 deletions(-) create mode 100644 lib/app/view/discord_connect_content.dart delete mode 100644 lib/common/view/bandwidth_dialog.dart create mode 100644 lib/common/view/data_safe_mode_dialog.dart diff --git a/lib/app/app_model.dart b/lib/app/app_model.dart index 7362c97b7..c4195cf2e 100644 --- a/lib/app/app_model.dart +++ b/lib/app/app_model.dart @@ -5,9 +5,11 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../app_config.dart'; +import '../common/view/snackbars.dart'; import '../constants.dart'; import '../expose/expose_service.dart'; import '../settings/settings_service.dart'; +import 'view/discord_connect_content.dart'; class AppModel extends SafeChangeNotifier { AppModel({ @@ -163,3 +165,13 @@ class AppModel extends SafeChangeNotifier { ]; } } + +void discordConnectedHandler(context, snapshot, cancel) { + if (snapshot.data == true) { + showSnackBar( + context: context, + duration: const Duration(seconds: 3), + content: DiscordConnectContent(connected: snapshot.data == true), + ); + } +} diff --git a/lib/app/connectivity_model.dart b/lib/app/connectivity_model.dart index 50e86ba90..44784dba2 100644 --- a/lib/app/connectivity_model.dart +++ b/lib/app/connectivity_model.dart @@ -1,11 +1,16 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; +import 'package:watch_it/watch_it.dart'; import '../common/data/audio_type.dart'; +import '../common/view/snackbars.dart'; import '../extensions/connectivity_x.dart'; +import '../player/player_model.dart'; import '../player/player_service.dart'; +import '../settings/settings_model.dart'; class ConnectivityModel extends SafeChangeNotifier { ConnectivityModel({ @@ -16,6 +21,8 @@ class ConnectivityModel extends SafeChangeNotifier { final PlayerService _playerService; final Connectivity _connectivity; + Stream> get onConnectivityChanged => + _connectivity.onConnectivityChanged; StreamSubscription>? _connectivitySubscription; Future init() async { @@ -35,7 +42,7 @@ class ConnectivityModel extends SafeChangeNotifier { bool get isOnline => _connectivity.isOnline(_result); - bool get isMaybeLowBandWidth => _connectivity.isMaybeLowBandWidth(_result); + bool get isMaybeLowBandWidth => _connectivity.isNotWifiNorEthernet(_result); List? get result => _result; List? _result; @@ -54,3 +61,31 @@ class ConnectivityModel extends SafeChangeNotifier { super.dispose(); } } + +void onConnectivityChangedHandler(context, res, cancel) { + final l10n = context.l10n; + final dataSafeMode = di().dataSafeMode; + final notifyDataSafeMode = di().notifyDataSafeMode; + if (!res.hasData || !context.mounted || !notifyDataSafeMode) { + return; + } + + if (!dataSafeMode && di().isNotWifiNorEthernet(res.data)) { + di().setDataSafeMode(true); + showSnackBar( + context: context, + snackBar: SnackBar( + content: Text(l10n.dataSafeModeEnabled), + ), + ); + } else if (dataSafeMode && + !di().isNotWifiNorEthernet(res.data)) { + di().setDataSafeMode(false); + showSnackBar( + context: context, + snackBar: SnackBar( + content: Text(l10n.dataSafeModeDisabled), + ), + ); + } +} diff --git a/lib/app/view/desktop_home_page.dart b/lib/app/view/desktop_home_page.dart index 124cfb88b..4d03bf8ea 100644 --- a/lib/app/view/desktop_home_page.dart +++ b/lib/app/view/desktop_home_page.dart @@ -1,21 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/yaru.dart'; import '../../app_config.dart'; -import '../../common/view/snackbars.dart'; -import '../../common/view/theme.dart'; import '../../common/view/ui_constants.dart'; import '../../extensions/build_context_x.dart'; -import '../../l10n/l10n.dart'; import '../../patch_notes/patch_notes_dialog.dart'; import '../../player/player_model.dart'; import '../../player/view/player_view.dart'; import '../../podcasts/download_model.dart'; import '../../podcasts/podcast_model.dart'; -import '../../podcasts/podcast_search_state.dart'; -import '../../podcasts/view/podcast_snackbar_contents.dart'; import '../../settings/settings_model.dart'; import '../app_model.dart'; import '../connectivity_model.dart'; @@ -58,55 +51,25 @@ class _DesktopHomePageState extends State { if (allowDiscordRPC && enableDiscordRPC) { registerStreamHandler( select: (AppModel m) => m.isDiscordConnectedStream, - handler: (context, snapshot, cancel) { - if (snapshot.data == true) { - showSnackBar( - context: context, - duration: const Duration(seconds: 3), - content: _DiscordConnectContent(connected: snapshot.data == true), - ); - } - }, + handler: discordConnectedHandler, ); } registerStreamHandler( select: (DownloadModel m) => m.messageStream, initialValue: null, - handler: (context, snapshot, cancel) { - if (snapshot.hasData) { - showSnackBar(context: context, content: Text(snapshot.data ?? '')); - } - }, + handler: downloadMessageStreamHandler, ); registerStreamHandler( select: (PodcastModel m) => m.stateStream, initialValue: null, - handler: (context, newValue, cancel) { - if (newValue.hasData) { - if (newValue.data == PodcastSearchState.done) { - ScaffoldMessenger.of(context).clearSnackBars(); - } else { - showSnackBar( - context: context, - content: switch (newValue.data) { - PodcastSearchState.loading => - const PodcastSearchLoadingSnackBarContent(), - PodcastSearchState.empty => - const PodcastSearchEmptyFeedSnackBarContent(), - PodcastSearchState.timeout => - const PodcastSearchTimeoutSnackBarContent(), - _ => const SizedBox.shrink() - }, - duration: switch (newValue.data) { - PodcastSearchState.loading => const Duration(seconds: 1000), - _ => const Duration(seconds: 3), - }, - ); - } - } - }, + handler: podcastStateStreamHandler, + ); + + registerStreamHandler( + select: (ConnectivityModel m) => m.onConnectivityChanged, + handler: onConnectivityChangedHandler, ); return Stack( @@ -139,32 +102,3 @@ class _DesktopHomePageState extends State { ); } } - -class _DiscordConnectContent extends StatelessWidget { - const _DiscordConnectContent({required this.connected}); - - final bool connected; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return Row( - mainAxisSize: MainAxisSize.min, - children: space( - widthGap: 10, - children: [ - Text( - '${connected ? l10n.connectedTo : l10n.disconnectedFrom}' - ' ${l10n.exposeToDiscordTitle}', - ), - Icon( - TablerIcons.brand_discord_filled, - color: context.theme.snackBarTheme.backgroundColor != null - ? contrastColor(context.theme.snackBarTheme.backgroundColor!) - : null, - ), - ], - ), - ); - } -} diff --git a/lib/app/view/discord_connect_content.dart b/lib/app/view/discord_connect_content.dart new file mode 100644 index 000000000..d5bcb46ce --- /dev/null +++ b/lib/app/view/discord_connect_content.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:yaru/yaru.dart'; +import '../../common/view/theme.dart'; +import '../../extensions/build_context_x.dart'; +import '../../l10n/l10n.dart'; + +class DiscordConnectContent extends StatelessWidget { + const DiscordConnectContent({super.key, required this.connected}); + + final bool connected; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Row( + mainAxisSize: MainAxisSize.min, + children: space( + widthGap: 10, + children: [ + Text( + '${connected ? l10n.connectedTo : l10n.disconnectedFrom}' + ' ${l10n.exposeToDiscordTitle}', + ), + Icon( + TablerIcons.brand_discord_filled, + color: context.theme.snackBarTheme.backgroundColor != null + ? contrastColor(context.theme.snackBarTheme.backgroundColor!) + : null, + ), + ], + ), + ); + } +} diff --git a/lib/app/view/mobile_page.dart b/lib/app/view/mobile_page.dart index 8e8f7a17c..850af42eb 100644 --- a/lib/app/view/mobile_page.dart +++ b/lib/app/view/mobile_page.dart @@ -1,21 +1,15 @@ -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import '../../common/view/bandwidth_dialog.dart'; -import '../../common/view/snackbars.dart'; import '../../common/view/theme.dart'; import '../../extensions/build_context_x.dart'; -import '../../extensions/connectivity_x.dart'; import '../../player/player_model.dart'; import '../../player/view/full_height_player.dart'; import '../../player/view/player_view.dart'; import '../../podcasts/download_model.dart'; import '../../podcasts/podcast_model.dart'; -import '../../podcasts/podcast_search_state.dart'; -import '../../podcasts/view/podcast_snackbar_contents.dart'; -import '../../settings/settings_model.dart'; import '../app_model.dart'; +import '../connectivity_model.dart'; import 'mobile_bottom_bar.dart'; class MobilePage extends StatelessWidget with WatchItMixin { @@ -34,67 +28,18 @@ class MobilePage extends StatelessWidget with WatchItMixin { registerStreamHandler( select: (DownloadModel m) => m.messageStream, initialValue: null, - handler: (context, snapshot, cancel) { - if (snapshot.hasData) { - showSnackBar(context: context, content: Text(snapshot.data ?? '')); - } - }, + handler: downloadMessageStreamHandler, ); - final dataSafeMode = watchPropertyValue((PlayerModel m) => m.dataSafeMode); - final notifyDataSafeMode = - watchPropertyValue((SettingsModel m) => m.notifyDataSafeMode); - registerStreamHandler( - select: (Connectivity m) => m.onConnectivityChanged, - handler: (context, res, cancel) { - if (notifyDataSafeMode && res.hasData) { - if (di().isMaybeLowBandWidth(res.data) && - !dataSafeMode) { - showDialog( - context: context, - builder: (context) => const BandwidthDialog(), - ); - } else if (!di().isMaybeLowBandWidth(res.data) && - dataSafeMode) { - showDialog( - context: context, - builder: (context) => const BandwidthDialog( - backOnBetterConnection: true, - ), - ); - } - } - }, + select: (PodcastModel m) => m.stateStream, + initialValue: null, + handler: podcastStateStreamHandler, ); registerStreamHandler( - select: (PodcastModel m) => m.stateStream, - initialValue: null, - handler: (context, newValue, cancel) { - if (newValue.hasData) { - if (newValue.data == PodcastSearchState.done) { - ScaffoldMessenger.of(context).clearSnackBars(); - } else { - showSnackBar( - context: context, - content: switch (newValue.data) { - PodcastSearchState.loading => - const PodcastSearchLoadingSnackBarContent(), - PodcastSearchState.empty => - const PodcastSearchEmptyFeedSnackBarContent(), - PodcastSearchState.timeout => - const PodcastSearchTimeoutSnackBarContent(), - _ => const SizedBox.shrink() - }, - duration: switch (newValue.data) { - PodcastSearchState.loading => const Duration(seconds: 1000), - _ => const Duration(seconds: 3), - }, - ); - } - } - }, + select: (ConnectivityModel m) => m.onConnectivityChanged, + handler: onConnectivityChangedHandler, ); return Scaffold( diff --git a/lib/common/view/bandwidth_dialog.dart b/lib/common/view/bandwidth_dialog.dart deleted file mode 100644 index e6b6c75a6..000000000 --- a/lib/common/view/bandwidth_dialog.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../../l10n/l10n.dart'; -import '../../player/player_model.dart'; -import '../../settings/settings_model.dart'; -import 'confirm.dart'; - -class BandwidthDialog extends StatelessWidget { - const BandwidthDialog({ - super.key, - this.backOnBetterConnection = false, - }); - - final bool backOnBetterConnection; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return ConfirmationDialog( - title: Text( - backOnBetterConnection - ? l10n.isBackInWifiDialogTitle - : l10n.isMaybeLowBandwidthDialogTitle, - ), - content: Text( - backOnBetterConnection - ? l10n.isBackInWifiDialogBody - : l10n.isMaybeLowBandwidthDialogBody, - ), - onConfirm: () async => - di().setDataSafeMode(!backOnBetterConnection), - cancelLabel: l10n.stopToNotifyAboutDataSafeMode, - onCancel: () async => di().setNotifyDataSafeMode(false), - ); - } -} diff --git a/lib/common/view/data_safe_mode_dialog.dart b/lib/common/view/data_safe_mode_dialog.dart new file mode 100644 index 000000000..e8eb2a0e7 --- /dev/null +++ b/lib/common/view/data_safe_mode_dialog.dart @@ -0,0 +1,44 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; + +import '../../app/connectivity_model.dart'; +import '../../extensions/connectivity_x.dart'; +import '../../l10n/l10n.dart'; +import '../../player/player_model.dart'; +import '../../settings/settings_model.dart'; +import 'confirm.dart'; + +class DataSafeModeDialog extends StatelessWidget with WatchItMixin { + const DataSafeModeDialog({super.key, required this.cancel}); + + final void Function() cancel; + + @override + Widget build(BuildContext context) { + final isEthernetOrWifi = watchPropertyValue( + (ConnectivityModel m) => + !di().isNotWifiNorEthernet(m.result), + ); + + final l10n = context.l10n; + return ConfirmationDialog( + title: Text( + isEthernetOrWifi + ? l10n.isBackInWifiDialogTitle + : l10n.isMaybeLowBandwidthDialogTitle, + ), + content: Text( + isEthernetOrWifi + ? l10n.isBackInWifiDialogBody + : l10n.isMaybeLowBandwidthDialogBody, + ), + onConfirm: () => di().setDataSafeMode(!isEthernetOrWifi), + cancelLabel: l10n.stopToNotifyAboutDataSafeMode, + onCancel: () { + di().setNotifyDataSafeMode(false); + cancel(); + }, + ); + } +} diff --git a/lib/extensions/connectivity_x.dart b/lib/extensions/connectivity_x.dart index e27f0da62..d3cf78749 100644 --- a/lib/extensions/connectivity_x.dart +++ b/lib/extensions/connectivity_x.dart @@ -8,7 +8,7 @@ extension ConnectivityX on Connectivity { res?.contains(ConnectivityResult.vpn) == true || res?.contains(ConnectivityResult.wifi) == true; - bool isMaybeLowBandWidth(List? res) => + bool isNotWifiNorEthernet(List? res) => res?.contains(ConnectivityResult.ethernet) == false && res?.contains(ConnectivityResult.wifi) == false; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e40ae7df0..a792217ca 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -189,6 +189,8 @@ "isBackInWifiDialogTitle": "Back in WIFI/Ethernet", "isBackInWifiDialogBody": "You are connected to WIFI or Ethernet. Do you want to disable data safe mode?", "enableDataSafeModeSettingTitle": "Data safe mode", + "dataSafeModeEnabled": "Mobile Connection: Data safe mode enabled.", + "dataSafeModeDisabled": "Wifi/Ethernet: Data safe mode disabled.", "enableDataSafeModeSettingDescription": "When active the player will not try to download artwork of titles send from radio stations.", "stopToNotifyAboutDataSafeMode": "Stop to notify me", "notifyMeAboutDataSafeModeTitle": "Data safe mode notifications", diff --git a/lib/player/player_service.dart b/lib/player/player_service.dart index 8de6d252d..cd74b71cd 100644 --- a/lib/player/player_service.dart +++ b/lib/player/player_service.dart @@ -816,7 +816,7 @@ class PlayerService { final songInfo = parsedIcyTitle.splitByDash; String? albumArt; if (!_dataSafeMode) { - await _onlineArtService.fetchAlbumArt(parsedIcyTitle); + albumArt = await _onlineArtService.fetchAlbumArt(parsedIcyTitle); } final mergedAudio = diff --git a/lib/player/view/player_main_controls.dart b/lib/player/view/player_main_controls.dart index 0debe739c..96c1b84f5 100644 --- a/lib/player/view/player_main_controls.dart +++ b/lib/player/view/player_main_controls.dart @@ -9,6 +9,7 @@ import '../../common/view/theme.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/theme_data_x.dart'; import '../../l10n/l10n.dart'; +import '../../radio/view/next_station_button.dart'; import '../player_model.dart'; import 'play_button.dart'; import 'repeat_button.dart'; @@ -116,12 +117,7 @@ class PlayerMainControls extends StatelessWidget with WatchItMixin { active: active, iconColor: defaultColor, ), - AudioType.radio => SizedBox.square( - dimension: context.theme.iconButtonTheme.style?.shape - ?.resolve({}) - ?.dimensions - .horizontal, - ), + AudioType.radio => const NextStationButton(), _ => const SizedBox.shrink(), }, ]; diff --git a/lib/podcasts/download_model.dart b/lib/podcasts/download_model.dart index ee0f52d47..a8037a7ed 100644 --- a/lib/podcasts/download_model.dart +++ b/lib/podcasts/download_model.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:flutter/widgets.dart'; import 'package:path/path.dart' as p; import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../common/data/audio.dart'; +import '../common/view/snackbars.dart'; import '../library/library_service.dart'; import '../settings/settings_service.dart'; @@ -161,3 +163,9 @@ class DownloadModel extends SafeChangeNotifier { super.dispose(); } } + +void downloadMessageStreamHandler(context, snapshot, cancel) { + if (snapshot.hasData) { + showSnackBar(context: context, content: Text(snapshot.data ?? '')); + } +} diff --git a/lib/podcasts/podcast_model.dart b/lib/podcasts/podcast_model.dart index b01fc82cd..d0c654fa9 100644 --- a/lib/podcasts/podcast_model.dart +++ b/lib/podcasts/podcast_model.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../common/data/audio.dart'; +import '../common/view/snackbars.dart'; import 'podcast_search_state.dart'; import 'podcast_service.dart'; +import 'view/podcast_snackbar_contents.dart'; class PodcastModel extends SafeChangeNotifier { PodcastModel({ @@ -118,3 +121,28 @@ class PodcastModel extends SafeChangeNotifier { await _searchStateController.close(); } } + +void podcastStateStreamHandler(context, newValue, cancel) { + if (newValue.hasData) { + if (newValue.data == PodcastSearchState.done) { + ScaffoldMessenger.of(context).clearSnackBars(); + } else { + showSnackBar( + context: context, + content: switch (newValue.data) { + PodcastSearchState.loading => + const PodcastSearchLoadingSnackBarContent(), + PodcastSearchState.empty => + const PodcastSearchEmptyFeedSnackBarContent(), + PodcastSearchState.timeout => + const PodcastSearchTimeoutSnackBarContent(), + _ => const SizedBox.shrink() + }, + duration: switch (newValue.data) { + PodcastSearchState.loading => const Duration(seconds: 1000), + _ => const Duration(seconds: 3), + }, + ); + } + } +} diff --git a/lib/radio/view/next_station_button.dart b/lib/radio/view/next_station_button.dart index 7f9e1d025..19acd6974 100644 --- a/lib/radio/view/next_station_button.dart +++ b/lib/radio/view/next_station_button.dart @@ -18,10 +18,9 @@ class NextStationButton extends StatelessWidget with WatchItMixin { return IconButton( tooltip: context.l10n.searchSimilarStation, - onPressed: loading + onPressed: loading || audio == null ? null : () { - if (audio == null) return; di().nextSimilarStation(audio).then( (station) { if (station == audio || audio.uuid == null) return; diff --git a/lib/search/search_model.dart b/lib/search/search_model.dart index a50179ae8..82503660c 100644 --- a/lib/search/search_model.dart +++ b/lib/search/search_model.dart @@ -178,49 +178,50 @@ class SearchModel extends SafeChangeNotifier { notifyListeners(); } - List? searchFromLanguage; - String? lastLanguage; - Future _findSimilarStation(Audio station) async { - if (searchFromLanguage == null || station.language != lastLanguage) { - searchFromLanguage = await _radioService.search( - limit: 1000, - language: station.language, - ); + final noNumbers = RegExp(r'^[^0-9]+$'); + Future _findSimilarStation(Audio audio) async { + final searchTags = audio.tags?.where((e) => noNumbers.hasMatch(e)); + if (searchTags == null || searchTags.isEmpty) { + return null; } - lastLanguage = station.language; - - final noNumbers = RegExp(r'^[^0-9]+$'); - return searchFromLanguage - ?.where( - (e) => _areTagsSimilar( - stationTags: station.tags?.where((e) => noNumbers.hasMatch(e)), - eTags: - Audio.fromStation(e).tags?.where((e) => noNumbers.hasMatch(e)), - ), - ) - .firstWhereOrNull((e) => e.stationUUID != station.uuid); + Station? maybe; + int tries = audio.tags!.length; + do { + maybe = (await _radioService.search( + limit: 500, + tag: searchTags.elementAt(Random().nextInt(searchTags.length)), + )) + ?.where( + (e) => _areTagsSimilar( + stationTags: searchTags, + otherTags: (Audio.fromStation(e).tags ?? []) + .where((e) => noNumbers.hasMatch(e)), + ), + ) + .lastWhereOrNull((e) => e.stationUUID != audio.uuid); + + tries--; + } while (tries > 0 && (maybe == null || audio == Audio.fromStation(maybe))); + + return maybe; } bool _areTagsSimilar({ - required Iterable? stationTags, - required Iterable? eTags, + required Iterable stationTags, + required Iterable otherTags, }) { - if (eTags == null || - eTags.isEmpty || - stationTags == null || - stationTags.length < 2) { - return false; - } - - final random = Random(); - final randomOne = random.nextInt(stationTags.length); - var randomTwo = random.nextInt(stationTags.length); - while (randomTwo == randomOne) { - randomTwo = random.nextInt(stationTags.length); + final matches = {}; + for (var tag in stationTags.map((e) => e.toLowerCase().trim()).toList()) { + if (otherTags.contains(tag.toLowerCase().trim())) { + matches.add(tag); + } } - return eTags.contains(stationTags.elementAt(randomOne)) && - eTags.contains(stationTags.elementAt(randomTwo)); + return switch (stationTags.length) { + 1 || 2 || 3 => matches.isNotEmpty, + 4 || 5 || 6 || 7 || 8 || 9 || 10 => matches.length >= 2, + _ => matches.length >= 3, + }; } Future