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:
zarzet
2026-03-31 18:54:29 +07:00
parent c936bd7dd0
commit a1aa1319ce
21 changed files with 182 additions and 26 deletions
+4
View File
@@ -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,
+4 -1
View File
@@ -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,
+6 -8
View File
@@ -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;
}