From 1998a9300ee3e0b97b2e5bb9fbd48d841fa1fa0c Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 10 Jul 2026 05:58:25 +0700 Subject: [PATCH] feat(downloads): support up to 3 concurrent downloads The Dart queue loop was already built around an activeDownloads map and per-item progress tracking but was gated to one download at a time. Add a concurrentDownloads setting (1-3, default 1) exposed in Download Settings > Performance, and let the scheduler keep up to that many items in flight. The experimental Android native worker is still strictly sequential, so it is skipped in favor of the Dart queue while concurrency is enabled. --- lib/l10n/app_localizations.dart | 24 ++++++ lib/l10n/app_localizations_ar.dart | 15 ++++ lib/l10n/app_localizations_de.dart | 15 ++++ lib/l10n/app_localizations_en.dart | 15 ++++ lib/l10n/app_localizations_es.dart | 15 ++++ lib/l10n/app_localizations_fr.dart | 15 ++++ lib/l10n/app_localizations_hi.dart | 15 ++++ lib/l10n/app_localizations_id.dart | 15 ++++ lib/l10n/app_localizations_ja.dart | 15 ++++ lib/l10n/app_localizations_ko.dart | 15 ++++ lib/l10n/app_localizations_nl.dart | 15 ++++ lib/l10n/app_localizations_pt.dart | 15 ++++ lib/l10n/app_localizations_ru.dart | 15 ++++ lib/l10n/app_localizations_tr.dart | 15 ++++ lib/l10n/app_localizations_uk.dart | 15 ++++ lib/l10n/app_localizations_zh.dart | 15 ++++ lib/l10n/arb/app_en.arb | 21 +++++ lib/models/settings.dart | 5 ++ lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 18 +++-- lib/providers/settings_provider.dart | 5 ++ .../settings/download_settings_page.dart | 81 +++++++++++++++++++ 22 files changed, 376 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cf6c9e24..fb6d384f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3074,6 +3074,30 @@ abstract class AppLocalizations { /// **'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'** String get settingsDownloadNetworkSubtitle; + /// Setting title - how many tracks download at the same time + /// + /// In en, this message translates to: + /// **'Concurrent downloads'** + String get settingsConcurrentDownloads; + + /// Subtitle explaining the concurrent downloads picker + /// + /// In en, this message translates to: + /// **'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'** + String get settingsConcurrentDownloadsSubtitle; + + /// Concurrent downloads option - sequential + /// + /// In en, this message translates to: + /// **'1 track at a time'** + String get concurrentDownloadsOne; + + /// Concurrent downloads option - parallel + /// + /// In en, this message translates to: + /// **'Up to {count} tracks at once'** + String concurrentDownloadsCount(int count); + /// Album folder option /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 6ed4a9c2..b48f479f 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsAr extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9e8362e0..5cf13fea 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1683,6 +1683,21 @@ class AppLocalizationsDe extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Wähle aus, welches Netzwerk für Downloads verwendet werden soll. Wenn nur WLAN aktiviert wird, werden Downloads auf mobilen Daten angehalten.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Künstler/Album'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 48cf7463..baa78d81 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 353bb1aa..bc9af73e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsEs extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e6a8ad3b..3c27b45d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1706,6 +1706,21 @@ class AppLocalizationsFr extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choisissez le réseau à utiliser pour les téléchargements. Si vous sélectionnez « Wi-Fi uniquement », les téléchargements seront interrompus lorsque vous utilisez les données mobiles.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artiste / Album'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 4200b1f8..50eca464 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsHi extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 79039cff..8a255c85 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1664,6 +1664,21 @@ class AppLocalizationsId extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Pilih jaringan mana yang akan digunakan untuk mengunduh. Jika diatur ke Hanya WiFi, unduhan akan berhenti sementara dan menggunakan data seluler.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artis / Album'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 39f1eab2..51110a4f 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1646,6 +1646,21 @@ class AppLocalizationsJa extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'アーティスト / アルバム'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 6394315b..47a26a22 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1643,6 +1643,21 @@ class AppLocalizationsKo extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 038d26f2..bed1c885 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsNl extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 0e2dcdfd..f4b4d29d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsPt extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b421bf75..47490bc0 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1681,6 +1681,21 @@ class AppLocalizationsRu extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Выберите, какую сеть использовать для скачивания. Когда установлено значение только WiFi — скачивания через мобильную сеть будут приостановлены.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Исполнитель / Альбом'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 3614bee6..4d8ba404 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1677,6 +1677,21 @@ class AppLocalizationsTr extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'İndirmeler için hangi ağın kullanılacağını seçin. Yalnızca WiFi olarak ayarlandığında, mobil veriye geçildiğinde indirmeler duraklatılır.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Sanatçı / Albüm'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 15b78e16..4f8fcd15 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1684,6 +1684,21 @@ class AppLocalizationsUk extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Вибрати мережу для завантажень. Якщо встановлено значення «Тільки Wi-Fi», завантаження призупиняться через мобільні дані.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Артист / Альбом'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7aca1bda..a62d4389 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1658,6 +1658,21 @@ class AppLocalizationsZh extends AppLocalizations { String get settingsDownloadNetworkSubtitle => 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override + String get settingsConcurrentDownloads => 'Concurrent downloads'; + + @override + String get settingsConcurrentDownloadsSubtitle => + 'Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.'; + + @override + String get concurrentDownloadsOne => '1 track at a time'; + + @override + String concurrentDownloadsCount(int count) { + return 'Up to $count tracks at once'; + } + @override String get albumFolderArtistAlbum => 'Artist / Album'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6f913b8a..4ba721f4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2175,6 +2175,27 @@ "@settingsDownloadNetworkSubtitle": { "description": "Subtitle explaining network preference" }, + "settingsConcurrentDownloads": "Concurrent downloads", + "@settingsConcurrentDownloads": { + "description": "Setting title - how many tracks download at the same time" + }, + "settingsConcurrentDownloadsSubtitle": "Downloading several tracks at once is faster, but some providers may rate-limit parallel requests.", + "@settingsConcurrentDownloadsSubtitle": { + "description": "Subtitle explaining the concurrent downloads picker" + }, + "concurrentDownloadsOne": "1 track at a time", + "@concurrentDownloadsOne": { + "description": "Concurrent downloads option - sequential" + }, + "concurrentDownloadsCount": "Up to {count} tracks at once", + "@concurrentDownloadsCount": { + "description": "Concurrent downloads option - parallel", + "placeholders": { + "count": { + "type": "int" + } + } + }, "albumFolderArtistAlbum": "Artist / Album", "@albumFolderArtistAlbum": { "description": "Album folder option" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index eb386a2e..e505bad6 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -62,6 +62,8 @@ class AppSettings { songLinkRegion; // SongLink userCountry region code used for platform lookup final bool nativeDownloadWorkerEnabled; // Experimental Android service-owned worker + final int + concurrentDownloads; // Max simultaneous downloads in the Dart queue (1-3) final bool localLibraryEnabled; final String localLibraryPath; @@ -140,6 +142,7 @@ class AppSettings { this.allowLocalNetwork = false, this.songLinkRegion = 'US', this.nativeDownloadWorkerEnabled = false, + this.concurrentDownloads = 1, this.localLibraryEnabled = false, this.localLibraryPath = '', this.localLibraryBookmark = '', @@ -208,6 +211,7 @@ class AppSettings { bool? allowLocalNetwork, String? songLinkRegion, bool? nativeDownloadWorkerEnabled, + int? concurrentDownloads, bool? localLibraryEnabled, String? localLibraryPath, String? localLibraryBookmark, @@ -289,6 +293,7 @@ class AppSettings { songLinkRegion: songLinkRegion ?? this.songLinkRegion, nativeDownloadWorkerEnabled: nativeDownloadWorkerEnabled ?? this.nativeDownloadWorkerEnabled, + concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads, localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled, localLibraryPath: localLibraryPath ?? this.localLibraryPath, localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 293787dd..9ac1f0d8 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -62,6 +62,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( songLinkRegion: json['songLinkRegion'] as String? ?? 'US', nativeDownloadWorkerEnabled: json['nativeDownloadWorkerEnabled'] as bool? ?? false, + concurrentDownloads: (json['concurrentDownloads'] as num?)?.toInt() ?? 1, localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false, localLibraryPath: json['localLibraryPath'] as String? ?? '', localLibraryBookmark: json['localLibraryBookmark'] as String? ?? '', @@ -138,6 +139,7 @@ Map _$AppSettingsToJson( 'allowLocalNetwork': instance.allowLocalNetwork, 'songLinkRegion': instance.songLinkRegion, 'nativeDownloadWorkerEnabled': instance.nativeDownloadWorkerEnabled, + 'concurrentDownloads': instance.concurrentDownloads, 'localLibraryEnabled': instance.localLibraryEnabled, 'localLibraryPath': instance.localLibraryPath, 'localLibraryBookmark': instance.localLibraryBookmark, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index f6b4734b..6b5acd33 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -3742,6 +3742,12 @@ class DownloadQueueNotifier extends Notifier { if (!Platform.isAndroid || !settings.nativeDownloadWorkerEnabled) { return false; } + if (settings.concurrentDownloads > 1) { + // The experimental native worker downloads strictly sequentially, so + // prefer the Dart queue when the user enabled concurrent downloads. + _log.i('Concurrent downloads enabled; skipping native worker'); + return false; + } if (!settings.useExtensionProviders) { return false; } @@ -5470,7 +5476,7 @@ class DownloadQueueNotifier extends Notifier { } try { - await _processQueueSequential(); + await _runQueueLoop(); } finally { if (iosDownloadBookmarkActive) { await PlatformBridge.stopAccessingIosBookmark(); @@ -5550,7 +5556,7 @@ class DownloadQueueNotifier extends Notifier { } } - Future _processQueueSequential() async { + Future _runQueueLoop() async { final activeDownloads = >{}; _startMultiProgressPolling(); @@ -5582,9 +5588,11 @@ class DownloadQueueNotifier extends Notifier { break; } - // One download at a time: only start the next item once the current - // download has finished, to stay within the API's single-request limit. - if (activeDownloads.isEmpty && + final maxConcurrent = ref + .read(settingsProvider) + .concurrentDownloads + .clamp(1, 3); + while (activeDownloads.length < maxConcurrent && queuedItems.isNotEmpty && !state.isPaused) { final item = queuedItems.removeAt(0); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a9b717c9..720502ab 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -613,6 +613,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setConcurrentDownloads(int count) { + state = state.copyWith(concurrentDownloads: count.clamp(1, 3)); + _saveSettings(); + } + void setNetworkCompatibilityMode(bool enabled) { state = state.copyWith(networkCompatibilityMode: enabled); _saveSettings(); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index c4a34081..af6c98e0 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -195,6 +195,20 @@ class _DownloadSettingsPageState extends ConsumerState { settings.downloadNetworkMode, ), ), + SettingsItem( + icon: Icons.dynamic_feed_outlined, + title: context.l10n.settingsConcurrentDownloads, + subtitle: settings.concurrentDownloads <= 1 + ? context.l10n.concurrentDownloadsOne + : context.l10n.concurrentDownloadsCount( + settings.concurrentDownloads, + ), + onTap: () => _showConcurrentDownloadsPicker( + context, + ref, + settings.concurrentDownloads, + ), + ), if (Platform.isAndroid) SettingsSwitchItem( icon: Icons.downloading_outlined, @@ -598,6 +612,73 @@ class _DownloadSettingsPageState extends ConsumerState { ); } + void _showConcurrentDownloadsPicker( + BuildContext context, + WidgetRef ref, + int current, + ) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.settingsConcurrentDownloads, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.settingsConcurrentDownloadsSubtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + for (final count in const [1, 2, 3]) + ListTile( + leading: Icon( + count == 1 + ? Icons.looks_one_outlined + : count == 2 + ? Icons.looks_two_outlined + : Icons.looks_3_outlined, + ), + title: Text( + count == 1 + ? context.l10n.concurrentDownloadsOne + : context.l10n.concurrentDownloadsCount(count), + ), + trailing: current == count + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setConcurrentDownloads(count); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + void _showSongLinkRegionPicker( BuildContext context, WidgetRef ref,