mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-25 09:14:17 +02:00
feat: add separate filename format for singles and EPs (#271)
Add singleFilenameFormat setting so singles/EPs can use a different filename template than albums. The format editor is reused with custom title/description. Dart selects the correct format based on track.isSingle before passing to Go, so no backend changes needed. Also fix isSingle getter to include all EPs regardless of totalTracks. Closes #271
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'フォルダ構成';
|
||||
|
||||
|
||||
@@ -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 => '폴더 분류 형식';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Организация папок';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> _$AppSettingsToJson(
|
||||
'searchProvider': instance.searchProvider,
|
||||
'homeFeedProvider': instance.homeFeedProvider,
|
||||
'separateSingles': instance.separateSingles,
|
||||
'singleFilenameFormat': instance.singleFilenameFormat,
|
||||
'albumFolderStructure': instance.albumFolderStructure,
|
||||
'showExtensionStore': instance.showExtensionStore,
|
||||
'locale': instance.locale,
|
||||
|
||||
@@ -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<String, dynamic> json) => _$TrackFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TrackToJson(this);
|
||||
|
||||
|
||||
bool get isFromExtension => source != null && source!.isNotEmpty;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DownloadQueueState> {
|
||||
? 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<DownloadQueueState> {
|
||||
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<DownloadQueueState> {
|
||||
? (trackToDownload.coverUrl ?? '')
|
||||
: '',
|
||||
outputDir: outputDir,
|
||||
filenameFormat: state.filenameFormat,
|
||||
filenameFormat: trackToDownload.isSingle
|
||||
? state.singleFilenameFormat
|
||||
: state.filenameFormat,
|
||||
quality: quality,
|
||||
embedMetadata: metadataEmbeddingEnabled,
|
||||
artistTagMode: settings.artistTagMode,
|
||||
|
||||
@@ -196,6 +196,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setSingleFilenameFormat(String format) {
|
||||
state = state.copyWith(singleFilenameFormat: format);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setDownloadDirectory(String directory) {
|
||||
state = state.copyWith(downloadDirectory: directory);
|
||||
_saveSettings();
|
||||
|
||||
@@ -592,6 +592,22 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
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<DownloadSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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<DownloadSettingsPage> {
|
||||
),
|
||||
),
|
||||
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<DownloadSettingsPage> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user