diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d722f825..488563dc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1456,6 +1456,18 @@ abstract class AppLocalizations { /// **'No organization'** String get folderOrganizationNone; + /// Folder option - playlist folders + /// + /// In en, this message translates to: + /// **'By Playlist'** + String get folderOrganizationByPlaylist; + + /// Subtitle for playlist folder option + /// + /// In en, this message translates to: + /// **'Separate folder for each playlist'** + String get folderOrganizationByPlaylistSubtitle; + /// Folder option - artist folders /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1bb33ef5..4cb5b75d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -785,6 +785,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get folderOrganizationNone => 'Keine Organisation'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'Nach Künstler'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bd9d4589..646f9a58 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -772,6 +772,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ed9236ad..e44fa9d8 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -772,6 +772,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d6a3b948..ae24b7e9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -774,6 +774,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index a461bd5b..08101eac 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -772,6 +772,13 @@ class AppLocalizationsHi extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index b668481b..cab860dd 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -775,6 +775,13 @@ class AppLocalizationsId extends AppLocalizations { @override String get folderOrganizationNone => 'Tidak ada'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'Berdasarkan Artis'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 71d10e1a..6ce38247 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -767,6 +767,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get folderOrganizationNone => '構成がありません'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'アーティスト別'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 43bfbce3..56287787 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -754,6 +754,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get folderOrganizationNone => '정리하지 않음'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2eeb00cc..5f4975d9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -772,6 +772,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 9436f6eb..62b648ae 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -772,6 +772,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 943b75f9..66c75d45 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -786,6 +786,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get folderOrganizationNone => 'Без организации'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'По исполнителю'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 223fecd3..3a88e179 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -777,6 +777,13 @@ class AppLocalizationsTr extends AppLocalizations { @override String get folderOrganizationNone => 'Organizasyon yok'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'Sanatçıya Göre'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 57f92319..3306bb1c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -772,6 +772,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get folderOrganizationNone => 'No organization'; + @override + String get folderOrganizationByPlaylist => 'By Playlist'; + + @override + String get folderOrganizationByPlaylistSubtitle => + 'Separate folder for each playlist'; + @override String get folderOrganizationByArtist => 'By Artist'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bb3036c9..03313d7b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1015,6 +1015,14 @@ "@folderOrganizationNone": { "description": "Folder option - flat structure" }, + "folderOrganizationByPlaylist": "By Playlist", + "@folderOrganizationByPlaylist": { + "description": "Folder option - playlist folders" + }, + "folderOrganizationByPlaylistSubtitle": "Separate folder for each playlist", + "@folderOrganizationByPlaylistSubtitle": { + "description": "Subtitle for playlist folder option" + }, "folderOrganizationByArtist": "By Artist", "@folderOrganizationByArtist": { "description": "Folder option - artist folders" diff --git a/lib/models/download_item.dart b/lib/models/download_item.dart index 4d8a650e..db55029b 100644 --- a/lib/models/download_item.dart +++ b/lib/models/download_item.dart @@ -34,6 +34,7 @@ class DownloadItem { final DownloadErrorType? errorType; final DateTime createdAt; final String? qualityOverride; // Override quality for this specific download + final String? playlistName; // Playlist context for folder organization const DownloadItem({ required this.id, @@ -48,6 +49,7 @@ class DownloadItem { this.errorType, required this.createdAt, this.qualityOverride, + this.playlistName, }); DownloadItem copyWith({ @@ -63,6 +65,7 @@ class DownloadItem { DownloadErrorType? errorType, DateTime? createdAt, String? qualityOverride, + String? playlistName, }) { return DownloadItem( id: id ?? this.id, @@ -77,6 +80,7 @@ class DownloadItem { errorType: errorType ?? this.errorType, createdAt: createdAt ?? this.createdAt, qualityOverride: qualityOverride ?? this.qualityOverride, + playlistName: playlistName ?? this.playlistName, ); } diff --git a/lib/models/download_item.g.dart b/lib/models/download_item.g.dart index 098290ce..961e6d6d 100644 --- a/lib/models/download_item.g.dart +++ b/lib/models/download_item.g.dart @@ -21,6 +21,7 @@ DownloadItem _$DownloadItemFromJson(Map json) => DownloadItem( errorType: $enumDecodeNullable(_$DownloadErrorTypeEnumMap, json['errorType']), createdAt: DateTime.parse(json['createdAt'] as String), qualityOverride: json['qualityOverride'] as String?, + playlistName: json['playlistName'] as String?, ); Map _$DownloadItemToJson(DownloadItem instance) => @@ -37,6 +38,7 @@ Map _$DownloadItemToJson(DownloadItem instance) => 'errorType': _$DownloadErrorTypeEnumMap[instance.errorType], 'createdAt': instance.createdAt.toIso8601String(), 'qualityOverride': instance.qualityOverride, + 'playlistName': instance.playlistName, }; const _$DownloadStatusEnumMap = { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 1e69a2fa..afb2e81c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1573,6 +1573,7 @@ class DownloadQueueNotifier extends Notifier { bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, + String? playlistName, }) async { String baseDir = state.outputDir; final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); @@ -1647,6 +1648,11 @@ class DownloadQueueNotifier extends Notifier { String subPath = ''; switch (folderOrganization) { + case 'playlist': + if (playlistName != null && playlistName.isNotEmpty) { + subPath = _sanitizeFolderName(playlistName); + } + break; case 'artist': final artistName = _sanitizeFolderName(folderArtist); subPath = artistName; @@ -1725,6 +1731,7 @@ class DownloadQueueNotifier extends Notifier { bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, bool filterContributingArtistsInAlbumArtist = false, + String? playlistName, }) async { final normalizedAlbumArtist = normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders @@ -1776,6 +1783,11 @@ class DownloadQueueNotifier extends Notifier { } switch (folderOrganization) { + case 'playlist': + if (playlistName != null && playlistName.isNotEmpty) { + return _sanitizeFolderName(playlistName); + } + return ''; case 'artist': return _sanitizeFolderName(folderArtist); case 'album': @@ -1900,7 +1912,7 @@ class DownloadQueueNotifier extends Notifier { ); } - String addToQueue(Track track, String service, {String? qualityOverride}) { + String addToQueue(Track track, String service, {String? qualityOverride, String? playlistName}) { final settings = ref.read(settingsProvider); updateSettings(settings); @@ -1912,6 +1924,7 @@ class DownloadQueueNotifier extends Notifier { service: service, createdAt: DateTime.now(), qualityOverride: qualityOverride, + playlistName: playlistName, ); state = state.copyWith(items: [...state.items, item]); @@ -1928,6 +1941,7 @@ class DownloadQueueNotifier extends Notifier { List tracks, String service, { String? qualityOverride, + String? playlistName, }) { final settings = ref.read(settingsProvider); updateSettings(settings); @@ -1942,6 +1956,7 @@ class DownloadQueueNotifier extends Notifier { service: service, createdAt: DateTime.now(), qualityOverride: qualityOverride, + playlistName: playlistName, ); }).toList(); @@ -3317,6 +3332,7 @@ class DownloadQueueNotifier extends Notifier { usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: settings.filterContributingArtistsInAlbumArtist, + playlistName: item.playlistName, ) : ''; String? appOutputDir; @@ -3331,6 +3347,7 @@ class DownloadQueueNotifier extends Notifier { usePrimaryArtistOnly: settings.usePrimaryArtistOnly, filterContributingArtistsInAlbumArtist: settings.filterContributingArtistsInAlbumArtist, + playlistName: item.playlistName, ); var effectiveOutputDir = initialOutputDir; var effectiveSafMode = isSafMode; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 7ffbd18e..37bf14cf 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -416,7 +416,7 @@ class _PlaylistScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addToQueue(track, service, qualityOverride: quality); + .addToQueue(track, service, qualityOverride: quality, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedToQueue(track.name)), @@ -427,7 +427,7 @@ class _PlaylistScreenState extends ConsumerState { } else { ref .read(downloadQueueProvider.notifier) - .addToQueue(track, settings.defaultService); + .addToQueue(track, settings.defaultService, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))), ); @@ -586,7 +586,7 @@ class _PlaylistScreenState extends ConsumerState { onSelect: (quality, service) { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, service, qualityOverride: quality); + .addMultipleToQueue(tracks, service, qualityOverride: quality, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -599,7 +599,7 @@ class _PlaylistScreenState extends ConsumerState { } else { ref .read(downloadQueueProvider.notifier) - .addMultipleToQueue(tracks, settings.defaultService); + .addMultipleToQueue(tracks, settings.defaultService, playlistName: widget.playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 1987bb2d..7a60f72c 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1405,6 +1405,8 @@ class _DownloadSettingsPageState extends ConsumerState { String _getFolderOrganizationLabel(String value) { switch (value) { + case 'playlist': + return 'By Playlist'; case 'artist': return 'By Artist'; case 'album': @@ -1982,6 +1984,18 @@ class _DownloadSettingsPageState extends ConsumerState { Navigator.pop(context); }, ), + _FolderOption( + title: context.l10n.folderOrganizationByPlaylist, + subtitle: context.l10n.folderOrganizationByPlaylistSubtitle, + example: 'SpotiFLAC/Playlist Name/Track.flac', + isSelected: current == 'playlist', + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization('playlist'); + Navigator.pop(context); + }, + ), _FolderOption( title: context.l10n.folderOrganizationByArtist, subtitle: context.l10n.folderOrganizationByArtistSubtitle,