diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 6559d666..48b6462f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -256,6 +256,18 @@ abstract class AppLocalizations { /// **'Filename Format'** String get downloadFilenameFormat; + /// Setting for output filename pattern for singles/EPs + /// + /// In en, this message translates to: + /// **'Single Filename Format'** + String get downloadSingleFilenameFormat; + + /// Subtitle description for single filename format setting + /// + /// In en, this message translates to: + /// **'Filename pattern for singles and EPs. Uses the same tags as the album format.'** + String get downloadSingleFilenameFormatDescription; + /// Title of the folder organization picker bottom sheet /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9ef37991..39838c14 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -76,6 +76,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadFilenameFormat => 'Dateinamenformat'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Ordnerstruktur'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7cc3d8a7..658f9916 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -75,6 +75,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadFilenameFormat => 'Filename Format'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 661553a2..dfbfd2c0 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -75,6 +75,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadFilenameFormat => 'Filename Format'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c50f8bf2..aa1f278c 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -75,6 +75,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadFilenameFormat => 'Nom du fichier'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Organisation du dossier'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index dcd5d24f..f8063b57 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -75,6 +75,13 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadFilenameFormat => 'Filename Format'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 1c1d0c1b..23fc4a35 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -76,6 +76,13 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadFilenameFormat => 'Format Nama File'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Organisasi Folder'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 06cc84af..12ad54f6 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -75,6 +75,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadFilenameFormat => 'ファイル名の形式'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'フォルダ構成'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index cd06f44d..03017d26 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -74,6 +74,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadFilenameFormat => '파일 이름 형식'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => '폴더 분류 형식'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f04b17e6..9ce0ffd9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -75,6 +75,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadFilenameFormat => 'Filename Format'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 3b023ae9..a523bad1 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -75,6 +75,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadFilenameFormat => 'Filename Format'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c5c61527..ad558078 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -76,6 +76,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadFilenameFormat => 'Формат имени файла'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Организация папок'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 25d24804..53958ef4 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -76,6 +76,13 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadFilenameFormat => 'Dosya adı formatı'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Dosya Organizasyonu'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b738f4ec..fe7ffc93 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -75,6 +75,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadFilenameFormat => 'Filename Format'; + @override + String get downloadSingleFilenameFormat => 'Single Filename Format'; + + @override + String get downloadSingleFilenameFormatDescription => + 'Filename pattern for singles and EPs. Uses the same tags as the album format.'; + @override String get downloadFolderOrganization => 'Folder Organization'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 5e78bbee..206d0319 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -89,6 +89,14 @@ "@downloadFilenameFormat": { "description": "Setting for output filename pattern" }, + "downloadSingleFilenameFormat": "Single Filename Format", + "@downloadSingleFilenameFormat": { + "description": "Setting for output filename pattern for singles/EPs" + }, + "downloadSingleFilenameFormatDescription": "Filename pattern for singles and EPs. Uses the same tags as the album format.", + "@downloadSingleFilenameFormatDescription": { + "description": "Subtitle description for single filename format setting" + }, "downloadFolderOrganization": "Folder Organization", "@downloadFolderOrganization": { "description": "Setting for folder structure" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 9a5a1ef5..40cc57ea 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -35,6 +35,7 @@ class AppSettings { final String? searchProvider; final String? homeFeedProvider; final bool separateSingles; + final String singleFilenameFormat; final String albumFolderStructure; final bool showExtensionStore; final String locale; @@ -108,6 +109,7 @@ class AppSettings { this.searchProvider, this.homeFeedProvider, this.separateSingles = false, + this.singleFilenameFormat = '{title} - {artist}', this.albumFolderStructure = 'artist_album', this.showExtensionStore = true, this.locale = 'system', @@ -170,6 +172,7 @@ class AppSettings { String? homeFeedProvider, bool clearHomeFeedProvider = false, bool? separateSingles, + String? singleFilenameFormat, String? albumFolderStructure, bool? showExtensionStore, String? locale, @@ -232,6 +235,7 @@ class AppSettings { ? null : (homeFeedProvider ?? this.homeFeedProvider), separateSingles: separateSingles ?? this.separateSingles, + singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat, albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure, showExtensionStore: showExtensionStore ?? this.showExtensionStore, locale: locale ?? this.locale, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 5987820f..3f9c5952 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -15,7 +15,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( downloadTreeUri: json['downloadTreeUri'] as String? ?? '', autoFallback: json['autoFallback'] as bool? ?? true, embedMetadata: json['embedMetadata'] as bool? ?? true, - artistTagMode: json['artistTagMode'] as String? ?? 'joined', + artistTagMode: json['artistTagMode'] as String? ?? artistTagModeJoined, embedLyrics: json['embedLyrics'] as bool? ?? true, maxQualityCover: json['maxQualityCover'] as bool? ?? true, isFirstLaunch: json['isFirstLaunch'] as bool? ?? true, @@ -37,6 +37,8 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( searchProvider: json['searchProvider'] as String?, homeFeedProvider: json['homeFeedProvider'] as String?, separateSingles: json['separateSingles'] as bool? ?? false, + singleFilenameFormat: + json['singleFilenameFormat'] as String? ?? '{title} - {artist}', albumFolderStructure: json['albumFolderStructure'] as String? ?? 'artist_album', showExtensionStore: json['showExtensionStore'] as bool? ?? true, @@ -104,6 +106,7 @@ Map _$AppSettingsToJson( 'searchProvider': instance.searchProvider, 'homeFeedProvider': instance.homeFeedProvider, 'separateSingles': instance.separateSingles, + 'singleFilenameFormat': instance.singleFilenameFormat, 'albumFolderStructure': instance.albumFolderStructure, 'showExtensionStore': instance.showExtensionStore, 'locale': instance.locale, diff --git a/lib/models/track.dart b/lib/models/track.dart index e4d850c2..f1ac89e0 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -49,26 +49,24 @@ class Track { bool get isSingle { switch (albumType?.toLowerCase()) { case 'single': - return true; case 'ep': - final count = totalTracks; - return count == null || count <= 1; + return true; default: return false; } } - + bool get isAlbumItem => itemType == 'album'; - + bool get isPlaylistItem => itemType == 'playlist'; - + bool get isArtistItem => itemType == 'artist'; - + bool get isCollection => isAlbumItem || isPlaylistItem || isArtistItem; factory Track.fromJson(Map json) => _$TrackFromJson(json); Map toJson() => _$TrackToJson(this); - + bool get isFromExtension => source != null && source!.isNotEmpty; } diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 1480cf75..d685a890 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1104,6 +1104,7 @@ class DownloadQueueState { final bool isPaused; final String outputDir; final String filenameFormat; + final String singleFilenameFormat; final String audioQuality; final bool autoFallback; final int concurrentDownloads; @@ -1115,6 +1116,7 @@ class DownloadQueueState { this.isPaused = false, this.outputDir = '', this.filenameFormat = '{artist} - {title}', + this.singleFilenameFormat = '{title} - {artist}', this.audioQuality = 'LOSSLESS', this.autoFallback = true, this.concurrentDownloads = 1, @@ -1127,6 +1129,7 @@ class DownloadQueueState { bool? isPaused, String? outputDir, String? filenameFormat, + String? singleFilenameFormat, String? audioQuality, bool? autoFallback, int? concurrentDownloads, @@ -1140,6 +1143,7 @@ class DownloadQueueState { isPaused: isPaused ?? this.isPaused, outputDir: outputDir ?? this.outputDir, filenameFormat: filenameFormat ?? this.filenameFormat, + singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat, audioQuality: audioQuality ?? this.audioQuality, autoFallback: autoFallback ?? this.autoFallback, concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads, @@ -2256,6 +2260,7 @@ class DownloadQueueNotifier extends Notifier { ? settings.downloadDirectory : state.outputDir, filenameFormat: settings.filenameFormat, + singleFilenameFormat: settings.singleFilenameFormat, audioQuality: settings.audioQuality, autoFallback: settings.autoFallback, concurrentDownloads: concurrentDownloads, @@ -3842,16 +3847,18 @@ class DownloadQueueNotifier extends Notifier { String? safBaseName; String safOutputExt = _determineOutputExt(quality, item.service); if (isSafMode) { - final baseName = - await PlatformBridge.buildFilename(state.filenameFormat, { - 'title': trackToDownload.name, - 'artist': trackToDownload.artistName, - 'album': trackToDownload.albumName, - 'track': trackToDownload.trackNumber ?? 0, - 'disc': trackToDownload.discNumber ?? 0, - 'year': _extractYear(trackToDownload.releaseDate) ?? '', - 'date': trackToDownload.releaseDate ?? '', - }); + final effectiveFormat = trackToDownload.isSingle + ? state.singleFilenameFormat + : state.filenameFormat; + final baseName = await PlatformBridge.buildFilename(effectiveFormat, { + 'title': trackToDownload.name, + 'artist': trackToDownload.artistName, + 'album': trackToDownload.albumName, + 'track': trackToDownload.trackNumber ?? 0, + 'disc': trackToDownload.discNumber ?? 0, + 'year': _extractYear(trackToDownload.releaseDate) ?? '', + 'date': trackToDownload.releaseDate ?? '', + }); final sanitized = await PlatformBridge.sanitizeFilename(baseName); safBaseName = sanitized; safFileName = '$sanitized$safOutputExt'; @@ -4214,7 +4221,9 @@ class DownloadQueueNotifier extends Notifier { ? (trackToDownload.coverUrl ?? '') : '', outputDir: outputDir, - filenameFormat: state.filenameFormat, + filenameFormat: trackToDownload.isSingle + ? state.singleFilenameFormat + : state.filenameFormat, quality: quality, embedMetadata: metadataEmbeddingEnabled, artistTagMode: settings.artistTagMode, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 055fc7a2..c2a6627d 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -196,6 +196,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setSingleFilenameFormat(String format) { + state = state.copyWith(singleFilenameFormat: format); + _saveSettings(); + } + void setDownloadDirectory(String directory) { state = state.copyWith(downloadDirectory: directory); _saveSettings(); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index a7fae66e..e4204171 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -592,6 +592,22 @@ class _DownloadSettingsPageState extends ConsumerState { settings.filenameFormat, ), ), + SettingsItem( + icon: Icons.music_note_outlined, + title: context.l10n.downloadSingleFilenameFormat, + subtitle: settings.singleFilenameFormat, + onTap: () => _showFormatEditor( + context, + ref, + settings.singleFilenameFormat, + onSave: ref + .read(settingsProvider.notifier) + .setSingleFilenameFormat, + title: context.l10n.downloadSingleFilenameFormat, + description: + context.l10n.downloadSingleFilenameFormatDescription, + ), + ), SettingsItem( icon: Icons.folder_outlined, title: context.l10n.downloadDirectory, @@ -952,7 +968,14 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - void _showFormatEditor(BuildContext context, WidgetRef ref, String current) { + void _showFormatEditor( + BuildContext context, + WidgetRef ref, + String current, { + void Function(String)? onSave, + String? title, + String? description, + }) { final controller = TextEditingController(text: current); final colorScheme = Theme.of(context).colorScheme; @@ -1035,14 +1058,14 @@ class _DownloadSettingsPageState extends ConsumerState { ), ), Text( - context.l10n.filenameFormat, + title ?? context.l10n.filenameFormat, style: Theme.of(context).textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( - context.l10n.downloadFilenameDescription, + description ?? context.l10n.downloadFilenameDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1149,9 +1172,12 @@ class _DownloadSettingsPageState extends ConsumerState { flex: 2, child: FilledButton( onPressed: () { - ref - .read(settingsProvider.notifier) - .setFilenameFormat(controller.text); + final save = + onSave ?? + ref + .read(settingsProvider.notifier) + .setFilenameFormat; + save(controller.text); Navigator.pop(context); }, style: FilledButton.styleFrom(