feat(playlists): local playlist folders (sidebar + page, DnD, view toggle) (#1119)

* feat(playlists): playlist folder model — store + pure grouping core

Local, per-server folder layer over the server's flat playlist list (the
Subsonic API has no folder concept). Adds:
- playlistFolders.ts: shared types + pure groupPlaylistsByFolder (used by
  every surface), with full unit coverage.
- playlistFolderStore.ts: persisted Zustand store (create/rename/delete/
  assign/collapse), per-server scoped; deleting a folder drops its
  assignments so playlists fall back to ungrouped.

UI surfaces (sidebar + Playlists page) build on this in following commits.

* i18n(playlists): folder strings across all 9 locales

Adds the playlists.folders.* namespace (folder names, move/remove, expand/
collapse, group-by-folders toggle, count plurals, and the local-only notice
explaining that Navidrome and the Subsonic API have no native folder support).

* feat(playlists): folder views, drag-to-folder, move-to-folder menu, view toggle

Surfaces the local folder layer on both the Playlists page and the sidebar:
- Page: collapsible folder sections + ungrouped remainder, each reusing
  VirtualCardGrid; flat grid is kept verbatim when no folders exist or the
  group view is toggled off.
- Drag-to-folder via the shared mouse-based psy-drop system (HTML5 DnD is
  unusable in WebKitGTK); the whole section is the drop zone, and the
  ungrouped zone appears during a drag so a playlist can always return to root.
- "Move to folder" submenu in the playlist context menu (keyboard-accessible
  path; also creates folders on the fly) — stays available offline.
- Header gets a "New folder" action and a "Group by folders" view toggle
  (both context-aware); a notice surfaces the local-only caveat.
- Sidebar renders the same collapsible folder groups.
- groupView preference added to the folder store.

* docs(changelog): note playlist folders (#1119)
This commit is contained in:
Psychotoxical
2026-06-17 21:22:30 +02:00
committed by GitHub
parent ccb2d11fc4
commit ad74578ef6
28 changed files with 1350 additions and 50 deletions
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} hinzugefügt, {{notFound}} nicht gefunden, {{duplicates}} Duplikate',
csvImportDownloadSuccess: 'Bericht erfolgreich heruntergeladen',
csvImportDownloadError: 'Bericht konnte nicht heruntergeladen werden',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Neuer Ordner',
namePlaceholder: 'Ordnername…',
create: 'Erstellen',
rename: 'Umbenennen',
delete: 'Ordner löschen',
ungrouped: 'Ohne Ordner',
moveToFolder: 'In Ordner verschieben',
removeFromFolder: 'Aus Ordner entfernen',
expandFolder: 'Ordner aufklappen',
collapseFolder: 'Ordner einklappen',
groupByFolders: 'Nach Ordnern gruppieren',
count_one: '{{count}} Playlist',
count_other: '{{count}} Playlists',
localOnlyNotice:
'Ordner werden nur in Psysonic auf diesem Gerät gespeichert. Navidrome und die Subsonic-API unterstützen keine Playlist-Ordner, daher wird die Ordnerstruktur nicht auf deinem Server gespeichert und nicht mit deinen anderen Geräten und Apps synchronisiert.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} added, {{notFound}} not found, {{duplicates}} duplicates',
csvImportDownloadSuccess: 'Report downloaded successfully',
csvImportDownloadError: 'Failed to download report',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'New folder',
namePlaceholder: 'Folder name…',
create: 'Create',
rename: 'Rename',
delete: 'Delete folder',
ungrouped: 'Ungrouped',
moveToFolder: 'Move to folder',
removeFromFolder: 'Remove from folder',
expandFolder: 'Expand folder',
collapseFolder: 'Collapse folder',
groupByFolders: 'Group by folders',
count_one: '{{count}} playlist',
count_other: '{{count}} playlists',
localOnlyNotice:
'Folders are saved only in Psysonic on this device. Navidrome and the Subsonic API have no native playlist-folder support, so the structure is not stored on your server or synced to your other devices and apps.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} agregadas, {{notFound}} no encontradas, {{duplicates}} duplicadas',
csvImportDownloadSuccess: 'Reporte descargado exitosamente',
csvImportDownloadError: 'Error al descargar el reporte',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Nueva carpeta',
namePlaceholder: 'Nombre de carpeta…',
create: 'Crear',
rename: 'Renombrar',
delete: 'Eliminar carpeta',
ungrouped: 'Sin carpeta',
moveToFolder: 'Mover a carpeta',
removeFromFolder: 'Quitar de la carpeta',
expandFolder: 'Expandir carpeta',
collapseFolder: 'Contraer carpeta',
groupByFolders: 'Agrupar por carpetas',
count_one: '{{count}} lista',
count_other: '{{count}} listas',
localOnlyNotice:
'Las carpetas se guardan solo en Psysonic en este dispositivo. Navidrome y la API de Subsonic no admiten carpetas de listas, por lo que la estructura no se guarda en tu servidor ni se sincroniza con tus otros dispositivos y aplicaciones.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} ajoutés, {{notFound}} non trouvés, {{duplicates}} doublons',
csvImportDownloadSuccess: 'Rapport téléchargé avec succès',
csvImportDownloadError: 'Échec du téléchargement du rapport',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Nouveau dossier',
namePlaceholder: 'Nom du dossier…',
create: 'Créer',
rename: 'Renommer',
delete: 'Supprimer le dossier',
ungrouped: 'Sans dossier',
moveToFolder: 'Déplacer vers un dossier',
removeFromFolder: 'Retirer du dossier',
expandFolder: 'Développer le dossier',
collapseFolder: 'Réduire le dossier',
groupByFolders: 'Grouper par dossiers',
count_one: '{{count}} playlist',
count_other: '{{count}} playlists',
localOnlyNotice:
'Les dossiers sont enregistrés uniquement dans Psysonic sur cet appareil. Navidrome et l\'API Subsonic ne prennent pas en charge les dossiers de playlists, la structure n\'est donc pas stockée sur ton serveur ni synchronisée avec tes autres appareils et applications.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} lagt til, {{notFound}} ikke funnet, {{duplicates}} duplikater',
csvImportDownloadSuccess: 'Rapport lastet ned',
csvImportDownloadError: 'Kunne ikke laste ned rapport',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Ny mappe',
namePlaceholder: 'Mappenavn…',
create: 'Opprett',
rename: 'Gi nytt navn',
delete: 'Slett mappe',
ungrouped: 'Uten mappe',
moveToFolder: 'Flytt til mappe',
removeFromFolder: 'Fjern fra mappe',
expandFolder: 'Utvid mappe',
collapseFolder: 'Skjul mappe',
groupByFolders: 'Grupper etter mapper',
count_one: '{{count}} spilleliste',
count_other: '{{count}} spillelister',
localOnlyNotice:
'Mapper lagres bare i Psysonic på denne enheten. Navidrome og Subsonic-API-et støtter ikke spillelistemapper, så strukturen lagres ikke på serveren din og synkroniseres ikke med dine andre enheter og apper.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} toegevoegd, {{notFound}} niet gevonden, {{duplicates}} duplicaten',
csvImportDownloadSuccess: 'Rapport succesvol gedownload',
csvImportDownloadError: 'Rapport downloaden mislukt',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Nieuwe map',
namePlaceholder: 'Mapnaam…',
create: 'Aanmaken',
rename: 'Hernoemen',
delete: 'Map verwijderen',
ungrouped: 'Zonder map',
moveToFolder: 'Naar map verplaatsen',
removeFromFolder: 'Uit map verwijderen',
expandFolder: 'Map uitvouwen',
collapseFolder: 'Map invouwen',
groupByFolders: 'Groeperen op mappen',
count_one: '{{count}} playlist',
count_other: '{{count}} playlists',
localOnlyNotice:
'Mappen worden alleen in Psysonic op dit apparaat opgeslagen. Navidrome en de Subsonic-API ondersteunen geen playlistmappen, dus de structuur wordt niet op je server opgeslagen of met je andere apparaten en apps gesynchroniseerd.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} adăugate, {{notFound}} negăsite, {{duplicates}} duplicate',
csvImportDownloadSuccess: 'Pagina de raport descărcată cu succes',
csvImportDownloadError: 'Nu s-a putut descărca raportul',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Folder nou',
namePlaceholder: 'Nume folder…',
create: 'Crează',
rename: 'Redenumește',
delete: 'Șterge folderul',
ungrouped: 'Fără folder',
moveToFolder: 'Mută în folder',
removeFromFolder: 'Scoate din folder',
expandFolder: 'Extinde folderul',
collapseFolder: 'Restrânge folderul',
groupByFolders: 'Grupează pe foldere',
count_one: '{{count}} playlist',
count_other: '{{count}} playlisturi',
localOnlyNotice:
'Folderele sunt salvate doar în Psysonic pe acest dispozitiv. Navidrome și API-ul Subsonic nu acceptă foldere de playlisturi, așa că structura nu este stocată pe serverul tău și nu se sincronizează cu celelalte dispozitive și aplicații ale tale.',
},
};
+18
View File
@@ -101,4 +101,22 @@ export const playlists = {
csvImportToast: '{{added}} добавлено, {{notFound}} не найдено, {{duplicates}} дубликатов',
csvImportDownloadSuccess: 'Отчёт успешно скачан',
csvImportDownloadError: 'Не удалось скачать отчёт',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: 'Новая папка',
namePlaceholder: 'Название папки…',
create: 'Создать',
rename: 'Переименовать',
delete: 'Удалить папку',
ungrouped: 'Без папки',
moveToFolder: 'Переместить в папку',
removeFromFolder: 'Убрать из папки',
expandFolder: 'Развернуть папку',
collapseFolder: 'Свернуть папку',
groupByFolders: 'Группировать по папкам',
count_one: '{{count}} плейлист',
count_other: '{{count}} плейлистов',
localOnlyNotice:
'Папки сохраняются только в Psysonic на этом устройстве. Navidrome и API Subsonic не поддерживают папки плейлистов, поэтому структура не хранится на твоём сервере и не синхронизируется с другими твоими устройствами и приложениями.',
},
};
+18
View File
@@ -99,4 +99,22 @@ export const playlists = {
csvImportToast: '{{added}} 已添加,{{notFound}} 未找到,{{duplicates}} 重复项',
csvImportDownloadSuccess: '报告下载成功',
csvImportDownloadError: '报告下载失败',
// Playlist folders (local, per-server organisation layer)
folders: {
newFolder: '新建文件夹',
namePlaceholder: '文件夹名称…',
create: '创建',
rename: '重命名',
delete: '删除文件夹',
ungrouped: '未分组',
moveToFolder: '移动到文件夹',
removeFromFolder: '从文件夹中移除',
expandFolder: '展开文件夹',
collapseFolder: '折叠文件夹',
groupByFolders: '按文件夹分组',
count_one: '{{count}} 个播放列表',
count_other: '{{count}} 个播放列表',
localOnlyNotice:
'文件夹仅保存在此设备上的 Psysonic 中。Navidrome 和 Subsonic API 不支持播放列表文件夹,因此该结构不会存储在你的服务器上,也不会同步到你的其他设备和应用。',
},
};