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.
This commit is contained in:
zarzet committed 2026-07-10 10:17:59 +07:00
1 parent 19f69a6090
commit 1998a9300e
22 files changed
+376 -5

No files matched your search

+24
View File
@@ -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.'** /// **'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'**
String get settingsDownloadNetworkSubtitle; 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 /// Album folder option
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsAr extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1683,6 +1683,21 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => 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.'; '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 @override
String get albumFolderArtistAlbum => 'Künstler/Album'; String get albumFolderArtistAlbum => 'Künstler/Album';
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsEs extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1706,6 +1706,21 @@ class AppLocalizationsFr extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => 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.'; '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 @override
String get albumFolderArtistAlbum => 'Artiste / Album'; String get albumFolderArtistAlbum => 'Artiste / Album';
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsHi extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1664,6 +1664,21 @@ class AppLocalizationsId extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Pilih jaringan mana yang akan digunakan untuk mengunduh. Jika diatur ke Hanya WiFi, unduhan akan berhenti sementara dan menggunakan data seluler.'; '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 @override
String get albumFolderArtistAlbum => 'Artis / Album'; String get albumFolderArtistAlbum => 'Artis / Album';
+15
View File
@@ -1646,6 +1646,21 @@ class AppLocalizationsJa extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'アーティスト / アルバム'; String get albumFolderArtistAlbum => 'アーティスト / アルバム';
+15
View File
@@ -1643,6 +1643,21 @@ class AppLocalizationsKo extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsNl extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsPt extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+15
View File
@@ -1681,6 +1681,21 @@ class AppLocalizationsRu extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Выберите, какую сеть использовать для скачивания. Когда установлено значение только WiFi — скачивания через мобильную сеть будут приостановлены.'; 'Выберите, какую сеть использовать для скачивания. Когда установлено значение только 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 @override
String get albumFolderArtistAlbum => 'Исполнитель / Альбом'; String get albumFolderArtistAlbum => 'Исполнитель / Альбом';
+15
View File
@@ -1677,6 +1677,21 @@ class AppLocalizationsTr extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => 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.'; 'İ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 @override
String get albumFolderArtistAlbum => 'Sanatçı / Albüm'; String get albumFolderArtistAlbum => 'Sanatçı / Albüm';
+15
View File
@@ -1684,6 +1684,21 @@ class AppLocalizationsUk extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Вибрати мережу для завантажень. Якщо встановлено значення «Тільки Wi-Fi», завантаження призупиняться через мобільні дані.'; 'Вибрати мережу для завантажень. Якщо встановлено значення «Тільки 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 @override
String get albumFolderArtistAlbum => 'Артист / Альбом'; String get albumFolderArtistAlbum => 'Артист / Альбом';
+15
View File
@@ -1658,6 +1658,21 @@ class AppLocalizationsZh extends AppLocalizations {
String get settingsDownloadNetworkSubtitle => String get settingsDownloadNetworkSubtitle =>
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; '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 @override
String get albumFolderArtistAlbum => 'Artist / Album'; String get albumFolderArtistAlbum => 'Artist / Album';
+21
View File
@@ -2175,6 +2175,27 @@
"@settingsDownloadNetworkSubtitle": { "@settingsDownloadNetworkSubtitle": {
"description": "Subtitle explaining network preference" "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": "Artist / Album",
"@albumFolderArtistAlbum": { "@albumFolderArtistAlbum": {
"description": "Album folder option" "description": "Album folder option"
+5
View File
@@ -62,6 +62,8 @@ class AppSettings {
songLinkRegion; // SongLink userCountry region code used for platform lookup songLinkRegion; // SongLink userCountry region code used for platform lookup
final bool final bool
nativeDownloadWorkerEnabled; // Experimental Android service-owned worker nativeDownloadWorkerEnabled; // Experimental Android service-owned worker
final int
concurrentDownloads; // Max simultaneous downloads in the Dart queue (1-3)
final bool localLibraryEnabled; final bool localLibraryEnabled;
final String localLibraryPath; final String localLibraryPath;
@@ -140,6 +142,7 @@ class AppSettings {
this.allowLocalNetwork = false, this.allowLocalNetwork = false,
this.songLinkRegion = 'US', this.songLinkRegion = 'US',
this.nativeDownloadWorkerEnabled = false, this.nativeDownloadWorkerEnabled = false,
this.concurrentDownloads = 1,
this.localLibraryEnabled = false, this.localLibraryEnabled = false,
this.localLibraryPath = '', this.localLibraryPath = '',
this.localLibraryBookmark = '', this.localLibraryBookmark = '',
@@ -208,6 +211,7 @@ class AppSettings {
bool? allowLocalNetwork, bool? allowLocalNetwork,
String? songLinkRegion, String? songLinkRegion,
bool? nativeDownloadWorkerEnabled, bool? nativeDownloadWorkerEnabled,
int? concurrentDownloads,
bool? localLibraryEnabled, bool? localLibraryEnabled,
String? localLibraryPath, String? localLibraryPath,
String? localLibraryBookmark, String? localLibraryBookmark,
@@ -289,6 +293,7 @@ class AppSettings {
songLinkRegion: songLinkRegion ?? this.songLinkRegion, songLinkRegion: songLinkRegion ?? this.songLinkRegion,
nativeDownloadWorkerEnabled: nativeDownloadWorkerEnabled:
nativeDownloadWorkerEnabled ?? this.nativeDownloadWorkerEnabled, nativeDownloadWorkerEnabled ?? this.nativeDownloadWorkerEnabled,
concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads,
localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled, localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled,
localLibraryPath: localLibraryPath ?? this.localLibraryPath, localLibraryPath: localLibraryPath ?? this.localLibraryPath,
localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark, localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark,
+2
View File
@@ -62,6 +62,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
songLinkRegion: json['songLinkRegion'] as String? ?? 'US', songLinkRegion: json['songLinkRegion'] as String? ?? 'US',
nativeDownloadWorkerEnabled: nativeDownloadWorkerEnabled:
json['nativeDownloadWorkerEnabled'] as bool? ?? false, json['nativeDownloadWorkerEnabled'] as bool? ?? false,
concurrentDownloads: (json['concurrentDownloads'] as num?)?.toInt() ?? 1,
localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false, localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false,
localLibraryPath: json['localLibraryPath'] as String? ?? '', localLibraryPath: json['localLibraryPath'] as String? ?? '',
localLibraryBookmark: json['localLibraryBookmark'] as String? ?? '', localLibraryBookmark: json['localLibraryBookmark'] as String? ?? '',
@@ -138,6 +139,7 @@ Map<String, dynamic> _$AppSettingsToJson(
'allowLocalNetwork': instance.allowLocalNetwork, 'allowLocalNetwork': instance.allowLocalNetwork,
'songLinkRegion': instance.songLinkRegion, 'songLinkRegion': instance.songLinkRegion,
'nativeDownloadWorkerEnabled': instance.nativeDownloadWorkerEnabled, 'nativeDownloadWorkerEnabled': instance.nativeDownloadWorkerEnabled,
'concurrentDownloads': instance.concurrentDownloads,
'localLibraryEnabled': instance.localLibraryEnabled, 'localLibraryEnabled': instance.localLibraryEnabled,
'localLibraryPath': instance.localLibraryPath, 'localLibraryPath': instance.localLibraryPath,
'localLibraryBookmark': instance.localLibraryBookmark, 'localLibraryBookmark': instance.localLibraryBookmark,
+13 -5
View File
@@ -3742,6 +3742,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (!Platform.isAndroid || !settings.nativeDownloadWorkerEnabled) { if (!Platform.isAndroid || !settings.nativeDownloadWorkerEnabled) {
return false; 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) { if (!settings.useExtensionProviders) {
return false; return false;
} }
@@ -5470,7 +5476,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
} }
try { try {
await _processQueueSequential(); await _runQueueLoop();
} finally { } finally {
if (iosDownloadBookmarkActive) { if (iosDownloadBookmarkActive) {
await PlatformBridge.stopAccessingIosBookmark(); await PlatformBridge.stopAccessingIosBookmark();
@@ -5550,7 +5556,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
} }
} }
Future<void> _processQueueSequential() async { Future<void> _runQueueLoop() async {
final activeDownloads = <String, Future<void>>{}; final activeDownloads = <String, Future<void>>{};
_startMultiProgressPolling(); _startMultiProgressPolling();
@@ -5582,9 +5588,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
break; break;
} }
// One download at a time: only start the next item once the current final maxConcurrent = ref
// download has finished, to stay within the API's single-request limit. .read(settingsProvider)
if (activeDownloads.isEmpty && .concurrentDownloads
.clamp(1, 3);
while (activeDownloads.length < maxConcurrent &&
queuedItems.isNotEmpty && queuedItems.isNotEmpty &&
!state.isPaused) { !state.isPaused) {
final item = queuedItems.removeAt(0); final item = queuedItems.removeAt(0);
+5
View File
@@ -613,6 +613,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings(); _saveSettings();
} }
void setConcurrentDownloads(int count) {
state = state.copyWith(concurrentDownloads: count.clamp(1, 3));
_saveSettings();
}
void setNetworkCompatibilityMode(bool enabled) { void setNetworkCompatibilityMode(bool enabled) {
state = state.copyWith(networkCompatibilityMode: enabled); state = state.copyWith(networkCompatibilityMode: enabled);
_saveSettings(); _saveSettings();
@@ -195,6 +195,20 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
settings.downloadNetworkMode, 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) if (Platform.isAndroid)
SettingsSwitchItem( SettingsSwitchItem(
icon: Icons.downloading_outlined, icon: Icons.downloading_outlined,
@@ -598,6 +612,73 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
); );
} }
void _showConcurrentDownloadsPicker(
BuildContext context,
WidgetRef ref,
int current,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet<void>(
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( void _showSongLinkRegionPicker(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,