feat: add 'By Playlist' folder organization option (#111)

This commit is contained in:
zarzet
2026-03-11 01:06:26 +07:00
parent c2736a61fb
commit f2ae1398db
20 changed files with 153 additions and 5 deletions
+12
View File
@@ -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:
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'アーティスト別';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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';
+7
View File
@@ -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 => 'По исполнителю';
+7
View File
@@ -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';
+7
View File
@@ -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';
+8
View File
@@ -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"
+4
View File
@@ -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,
);
}
+2
View File
@@ -21,6 +21,7 @@ DownloadItem _$DownloadItemFromJson(Map<String, dynamic> 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<String, dynamic> _$DownloadItemToJson(DownloadItem instance) =>
@@ -37,6 +38,7 @@ Map<String, dynamic> _$DownloadItemToJson(DownloadItem instance) =>
'errorType': _$DownloadErrorTypeEnumMap[instance.errorType],
'createdAt': instance.createdAt.toIso8601String(),
'qualityOverride': instance.qualityOverride,
'playlistName': instance.playlistName,
};
const _$DownloadStatusEnumMap = {
+18 -1
View File
@@ -1573,6 +1573,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
}
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<DownloadQueueState> {
);
}
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<DownloadQueueState> {
service: service,
createdAt: DateTime.now(),
qualityOverride: qualityOverride,
playlistName: playlistName,
);
state = state.copyWith(items: [...state.items, item]);
@@ -1928,6 +1941,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
List<Track> tracks,
String service, {
String? qualityOverride,
String? playlistName,
}) {
final settings = ref.read(settingsProvider);
updateSettings(settings);
@@ -1942,6 +1956,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
service: service,
createdAt: DateTime.now(),
qualityOverride: qualityOverride,
playlistName: playlistName,
);
}).toList();
@@ -3317,6 +3332,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
usePrimaryArtistOnly: settings.usePrimaryArtistOnly,
filterContributingArtistsInAlbumArtist:
settings.filterContributingArtistsInAlbumArtist,
playlistName: item.playlistName,
)
: '';
String? appOutputDir;
@@ -3331,6 +3347,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
usePrimaryArtistOnly: settings.usePrimaryArtistOnly,
filterContributingArtistsInAlbumArtist:
settings.filterContributingArtistsInAlbumArtist,
playlistName: item.playlistName,
);
var effectiveOutputDir = initialOutputDir;
var effectiveSafMode = isSafMode;
+4 -4
View File
@@ -416,7 +416,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
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<PlaylistScreen> {
} 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<PlaylistScreen> {
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<PlaylistScreen> {
} 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)),
@@ -1405,6 +1405,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
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<DownloadSettingsPage> {
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,