mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(device-sync): USB/SD card sync page (WIP)
Adds a new Device Sync page for transferring music from Navidrome to USB drives or SD cards. Sidebar entry is hidden by default. - Four new Tauri commands: sync_track_to_device, compute_sync_paths, list_device_dir_files, delete_device_file - Filename template engine with cross-platform path sanitization - 4-concurrent-worker download, live progress via device:sync:progress event - Persistent source list (albums/playlists/artists) with checkbox deletion - Expandable artist tree in browser panel (per-album selection) - i18n: DE + EN complete; other locales have stub keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ export const deTranslation = {
|
||||
mostPlayed: 'Meistgehört',
|
||||
radio: 'Internetradio',
|
||||
folderBrowser: 'Ordner-Browser',
|
||||
deviceSync: 'Gerätesync',
|
||||
libraryScope: 'Bibliotheksumfang',
|
||||
allLibraries: 'Alle Bibliotheken',
|
||||
expandPlaylists: 'Playlists ausklappen',
|
||||
@@ -1021,4 +1022,35 @@ export const deTranslation = {
|
||||
empty: 'Leerer Ordner',
|
||||
error: 'Laden fehlgeschlagen',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Gerätesync',
|
||||
targetFolder: 'Zielordner',
|
||||
noFolderChosen: 'Kein Ordner gewählt',
|
||||
chooseFolder: 'Auswählen…',
|
||||
filenameTemplate: 'Dateinamen-Vorlage',
|
||||
templateHint: 'Variablen: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
onDevice: 'Auf dem Gerät',
|
||||
addSources: 'Hinzufügen…',
|
||||
colName: 'Name',
|
||||
colType: 'Typ',
|
||||
syncResult: '{{done}} übertragen, {{skipped}} bereits aktuell ({{total}} gesamt)',
|
||||
deleteFromDevice: 'Vom Gerät löschen ({{count}})',
|
||||
confirmDelete: '{{count}} Eintrag/Einträge vom Gerät löschen: {{names}}?',
|
||||
deleteComplete: '{{count}} Eintrag/Einträge vom Gerät entfernt.',
|
||||
selectedSources: 'Ausgewählte Quellen',
|
||||
noSourcesSelected: 'Keine Quellen ausgewählt. Klicke auf Einträge im Browser um sie hinzuzufügen.',
|
||||
clearAll: 'Alle entfernen',
|
||||
tabPlaylists: 'Wiedergabelisten',
|
||||
tabAlbums: 'Alben',
|
||||
tabArtists: 'Künstler',
|
||||
searchPlaceholder: 'Suche…',
|
||||
syncButton: 'Auf Gerät übertragen',
|
||||
cancel: 'Abbrechen',
|
||||
noTargetDir: 'Bitte zuerst einen Zielordner auswählen.',
|
||||
noSources: 'Bitte mindestens eine Quelle auswählen.',
|
||||
noTracks: 'Keine Tracks in den ausgewählten Quellen gefunden.',
|
||||
fetchError: 'Fehler beim Laden der Tracks vom Server.',
|
||||
syncComplete: 'Übertragung abgeschlossen.',
|
||||
dismiss: 'Schließen',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export const enTranslation = {
|
||||
mostPlayed: 'Most Played',
|
||||
radio: 'Internet Radio',
|
||||
folderBrowser: 'Folder Browser',
|
||||
deviceSync: 'Device Sync',
|
||||
libraryScope: 'Library scope',
|
||||
allLibraries: 'All libraries',
|
||||
expandPlaylists: 'Expand playlists',
|
||||
@@ -1023,4 +1024,35 @@ export const enTranslation = {
|
||||
empty: 'Empty folder',
|
||||
error: 'Failed to load',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Device Sync',
|
||||
targetFolder: 'Target Folder',
|
||||
noFolderChosen: 'No folder chosen',
|
||||
chooseFolder: 'Choose…',
|
||||
filenameTemplate: 'Filename Template',
|
||||
templateHint: 'Variables: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
onDevice: 'On Device',
|
||||
addSources: 'Add…',
|
||||
colName: 'Name',
|
||||
colType: 'Type',
|
||||
syncResult: '{{done}} transferred, {{skipped}} already up to date ({{total}} total)',
|
||||
deleteFromDevice: 'Delete from device ({{count}})',
|
||||
confirmDelete: 'Delete {{count}} item(s) from the device: {{names}}?',
|
||||
deleteComplete: '{{count}} item(s) removed from device.',
|
||||
selectedSources: 'Selected Sources',
|
||||
noSourcesSelected: 'No sources selected. Click items in the browser to add them.',
|
||||
clearAll: 'Clear all',
|
||||
tabPlaylists: 'Playlists',
|
||||
tabAlbums: 'Albums',
|
||||
tabArtists: 'Artists',
|
||||
searchPlaceholder: 'Search…',
|
||||
syncButton: 'Sync to Device',
|
||||
cancel: 'Cancel',
|
||||
noTargetDir: 'Please choose a target folder first.',
|
||||
noSources: 'Please select at least one source.',
|
||||
noTracks: 'No tracks found in the selected sources.',
|
||||
fetchError: 'Failed to fetch tracks from server.',
|
||||
syncComplete: 'Sync complete.',
|
||||
dismiss: 'Dismiss',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export const esTranslation = {
|
||||
mostPlayed: 'Más Reproducidos',
|
||||
radio: 'Radio por Internet',
|
||||
folderBrowser: 'Explorar Carpetas',
|
||||
deviceSync: 'Sincronizar dispositivo',
|
||||
libraryScope: 'Ámbito de biblioteca',
|
||||
allLibraries: 'Todas las bibliotecas',
|
||||
expandPlaylists: 'Expandir listas',
|
||||
@@ -1024,4 +1025,31 @@ export const esTranslation = {
|
||||
empty: 'Carpeta vacía',
|
||||
error: 'Error al cargar',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Sincronizar dispositivo',
|
||||
targetFolder: 'Carpeta de destino',
|
||||
noFolderChosen: 'Ninguna carpeta seleccionada',
|
||||
chooseFolder: 'Elegir…',
|
||||
filenameTemplate: 'Plantilla de nombre de archivo',
|
||||
templateHint: 'Variables: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
cleanupButton: 'Eliminar archivos fuera de la selección',
|
||||
cleanupNothingToDelete: 'Nada que eliminar — el dispositivo ya está sincronizado.',
|
||||
confirmCleanup: '¿Eliminar {{count}} archivo(s) del dispositivo que no están en la selección actual? ¿Continuar?',
|
||||
cleanupComplete: '{{count}} archivo(s) eliminado(s) del dispositivo.',
|
||||
selectedSources: 'Fuentes seleccionadas',
|
||||
noSourcesSelected: 'No hay fuentes seleccionadas. Haz clic en elementos del navegador para agregarlos.',
|
||||
clearAll: 'Limpiar todo',
|
||||
tabPlaylists: 'Listas de reproducción',
|
||||
tabAlbums: 'Álbumes',
|
||||
tabArtists: 'Artistas',
|
||||
searchPlaceholder: 'Buscar…',
|
||||
syncButton: 'Sincronizar al dispositivo',
|
||||
cancel: 'Cancelar',
|
||||
noTargetDir: 'Por favor, elige primero una carpeta de destino.',
|
||||
noSources: 'Por favor, selecciona al menos una fuente.',
|
||||
noTracks: 'No se encontraron pistas en las fuentes seleccionadas.',
|
||||
fetchError: 'Error al obtener pistas del servidor.',
|
||||
syncComplete: 'Sincronización completada.',
|
||||
dismiss: 'Cerrar',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export const frTranslation = {
|
||||
mostPlayed: 'Les plus joués',
|
||||
radio: 'Radio Internet',
|
||||
folderBrowser: 'Explorateur de dossiers',
|
||||
deviceSync: 'Sync appareil',
|
||||
libraryScope: 'Portée de la bibliothèque',
|
||||
allLibraries: 'Toutes les bibliothèques',
|
||||
expandPlaylists: 'Développer les playlists',
|
||||
@@ -1019,4 +1020,31 @@ export const frTranslation = {
|
||||
empty: 'Dossier vide',
|
||||
error: 'Échec du chargement',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Sync appareil',
|
||||
targetFolder: 'Dossier cible',
|
||||
noFolderChosen: 'Aucun dossier sélectionné',
|
||||
chooseFolder: 'Choisir…',
|
||||
filenameTemplate: 'Modèle de nom de fichier',
|
||||
templateHint: 'Variables : {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
cleanupButton: 'Supprimer les fichiers absents',
|
||||
cleanupNothingToDelete: 'Rien à supprimer — l\'appareil est déjà synchronisé.',
|
||||
confirmCleanup: 'Supprimer {{count}} fichier(s) de l\'appareil qui ne font pas partie de la sélection actuelle. Continuer ?',
|
||||
cleanupComplete: '{{count}} fichier(s) supprimé(s) de l\'appareil.',
|
||||
selectedSources: 'Sources sélectionnées',
|
||||
noSourcesSelected: 'Aucune source sélectionnée. Cliquez sur des éléments dans le navigateur pour les ajouter.',
|
||||
clearAll: 'Tout effacer',
|
||||
tabPlaylists: 'Listes de lecture',
|
||||
tabAlbums: 'Albums',
|
||||
tabArtists: 'Artistes',
|
||||
searchPlaceholder: 'Rechercher…',
|
||||
syncButton: 'Synchroniser vers l\'appareil',
|
||||
cancel: 'Annuler',
|
||||
noTargetDir: 'Veuillez d\'abord choisir un dossier cible.',
|
||||
noSources: 'Veuillez sélectionner au moins une source.',
|
||||
noTracks: 'Aucune piste trouvée dans les sources sélectionnées.',
|
||||
fetchError: 'Échec du chargement des pistes depuis le serveur.',
|
||||
syncComplete: 'Synchronisation terminée.',
|
||||
dismiss: 'Fermer',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export const nbTranslation = {
|
||||
mostPlayed: 'Mest spilt',
|
||||
radio: 'Internettradio',
|
||||
folderBrowser: 'Mappeleser',
|
||||
deviceSync: 'Enhetssynk',
|
||||
libraryScope: 'Biblioteksomfang',
|
||||
allLibraries: 'Alle biblioteker',
|
||||
expandPlaylists: 'Utvid spillelister',
|
||||
@@ -1018,4 +1019,31 @@ export const nbTranslation = {
|
||||
empty: 'Tom mappe',
|
||||
error: 'Kunne ikke laste',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Enhetssynk',
|
||||
targetFolder: 'Målmappe',
|
||||
noFolderChosen: 'Ingen mappe valgt',
|
||||
chooseFolder: 'Velg…',
|
||||
filenameTemplate: 'Filnavnmal',
|
||||
templateHint: 'Variabler: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
cleanupButton: 'Fjern filer som ikke er i utvalget',
|
||||
cleanupNothingToDelete: 'Ingenting å fjerne — enheten er allerede synkronisert.',
|
||||
confirmCleanup: 'Slette {{count}} fil(er) fra enheten som ikke er i det gjeldende utvalget. Fortsette?',
|
||||
cleanupComplete: '{{count}} fil(er) fjernet fra enheten.',
|
||||
selectedSources: 'Valgte kilder',
|
||||
noSourcesSelected: 'Ingen kilder valgt. Klikk på elementer i nettleseren for å legge dem til.',
|
||||
clearAll: 'Fjern alle',
|
||||
tabPlaylists: 'Spillelister',
|
||||
tabAlbums: 'Album',
|
||||
tabArtists: 'Artister',
|
||||
searchPlaceholder: 'Søk…',
|
||||
syncButton: 'Synkroniser til enhet',
|
||||
cancel: 'Avbryt',
|
||||
noTargetDir: 'Velg en målmappe først.',
|
||||
noSources: 'Velg minst én kilde.',
|
||||
noTracks: 'Ingen spor funnet i de valgte kildene.',
|
||||
fetchError: 'Kunne ikke hente spor fra serveren.',
|
||||
syncComplete: 'Synkronisering fullført.',
|
||||
dismiss: 'Lukk',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export const nlTranslation = {
|
||||
mostPlayed: 'Meest gespeeld',
|
||||
radio: 'Internetradio',
|
||||
folderBrowser: 'Mappenverkenner',
|
||||
deviceSync: 'Apparaatsync',
|
||||
libraryScope: 'Bibliotheekbereik',
|
||||
allLibraries: 'Alle bibliotheken',
|
||||
expandPlaylists: 'Afspeellijsten uitklappen',
|
||||
@@ -1016,4 +1017,31 @@ export const nlTranslation = {
|
||||
empty: 'Lege map',
|
||||
error: 'Laden mislukt',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Apparaatsync',
|
||||
targetFolder: 'Doelmap',
|
||||
noFolderChosen: 'Geen map gekozen',
|
||||
chooseFolder: 'Kiezen…',
|
||||
filenameTemplate: 'Bestandsnaamsjabloon',
|
||||
templateHint: 'Variabelen: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
cleanupButton: 'Niet-geselecteerde bestanden verwijderen',
|
||||
cleanupNothingToDelete: 'Niets te verwijderen — apparaat is al gesynchroniseerd.',
|
||||
confirmCleanup: '{{count}} bestand(en) verwijderen van het apparaat die niet in de huidige selectie staan. Doorgaan?',
|
||||
cleanupComplete: '{{count}} bestand(en) van het apparaat verwijderd.',
|
||||
selectedSources: 'Geselecteerde bronnen',
|
||||
noSourcesSelected: 'Geen bronnen geselecteerd. Klik op items in de browser om ze toe te voegen.',
|
||||
clearAll: 'Alles wissen',
|
||||
tabPlaylists: 'Afspeellijsten',
|
||||
tabAlbums: 'Albums',
|
||||
tabArtists: 'Artiesten',
|
||||
searchPlaceholder: 'Zoeken…',
|
||||
syncButton: 'Synchroniseren naar apparaat',
|
||||
cancel: 'Annuleren',
|
||||
noTargetDir: 'Kies eerst een doelmap.',
|
||||
noSources: 'Selecteer minimaal één bron.',
|
||||
noTracks: 'Geen nummers gevonden in de geselecteerde bronnen.',
|
||||
fetchError: 'Ophalen van nummers van de server mislukt.',
|
||||
syncComplete: 'Synchronisatie voltooid.',
|
||||
dismiss: 'Sluiten',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export const ruTranslation = {
|
||||
mostPlayed: 'Популярное',
|
||||
radio: 'Онлайн-радио',
|
||||
folderBrowser: 'Браузер папок',
|
||||
deviceSync: 'Синхронизация устройства',
|
||||
libraryScope: 'Область медиатеки',
|
||||
allLibraries: 'Все библиотеки',
|
||||
expandPlaylists: 'Развернуть плейлисты',
|
||||
@@ -1077,4 +1078,31 @@ export const ruTranslation = {
|
||||
empty: 'Папка пуста',
|
||||
error: 'Ошибка загрузки',
|
||||
},
|
||||
deviceSync: {
|
||||
title: 'Синхронизация устройства',
|
||||
targetFolder: 'Папка назначения',
|
||||
noFolderChosen: 'Папка не выбрана',
|
||||
chooseFolder: 'Выбрать…',
|
||||
filenameTemplate: 'Шаблон имени файла',
|
||||
templateHint: 'Переменные: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
|
||||
cleanupButton: 'Удалить файлы вне выборки',
|
||||
cleanupNothingToDelete: 'Нечего удалять — устройство уже синхронизировано.',
|
||||
confirmCleanup: 'Удалить {{count}} файл(ов) с устройства, не входящих в текущую выборку. Продолжить?',
|
||||
cleanupComplete: '{{count}} файл(ов) удалено с устройства.',
|
||||
selectedSources: 'Выбранные источники',
|
||||
noSourcesSelected: 'Источники не выбраны. Нажмите на элементы в браузере, чтобы добавить их.',
|
||||
clearAll: 'Очистить всё',
|
||||
tabPlaylists: 'Плейлисты',
|
||||
tabAlbums: 'Альбомы',
|
||||
tabArtists: 'Исполнители',
|
||||
searchPlaceholder: 'Поиск…',
|
||||
syncButton: 'Синхронизировать на устройство',
|
||||
cancel: 'Отмена',
|
||||
noTargetDir: 'Сначала выберите папку назначения.',
|
||||
noSources: 'Выберите хотя бы один источник.',
|
||||
noTracks: 'В выбранных источниках не найдено треков.',
|
||||
fetchError: 'Ошибка загрузки треков с сервера.',
|
||||
syncComplete: 'Синхронизация завершена.',
|
||||
dismiss: 'Закрыть',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export const zhTranslation = {
|
||||
mostPlayed: '最常播放',
|
||||
radio: '网络电台',
|
||||
folderBrowser: '文件夹浏览器',
|
||||
deviceSync: '设备同步',
|
||||
libraryScope: '资料库范围',
|
||||
allLibraries: '所有资料库',
|
||||
expandPlaylists: '展开播放列表',
|
||||
@@ -1012,4 +1013,31 @@ export const zhTranslation = {
|
||||
empty: '空文件夹',
|
||||
error: '加载失败',
|
||||
},
|
||||
deviceSync: {
|
||||
title: '设备同步',
|
||||
targetFolder: '目标文件夹',
|
||||
noFolderChosen: '未选择文件夹',
|
||||
chooseFolder: '选择…',
|
||||
filenameTemplate: '文件名模板',
|
||||
templateHint: '变量:{artist}、{album}、{title}、{track_number}、{disc_number}、{year}',
|
||||
cleanupButton: '删除不在选择范围内的文件',
|
||||
cleanupNothingToDelete: '无需删除 — 设备已同步。',
|
||||
confirmCleanup: '将从设备中删除 {{count}} 个不在当前选择范围内的文件。继续?',
|
||||
cleanupComplete: '已从设备中删除 {{count}} 个文件。',
|
||||
selectedSources: '已选来源',
|
||||
noSourcesSelected: '未选择来源。点击浏览器中的项目以添加。',
|
||||
clearAll: '全部清除',
|
||||
tabPlaylists: '播放列表',
|
||||
tabAlbums: '专辑',
|
||||
tabArtists: '艺术家',
|
||||
searchPlaceholder: '搜索…',
|
||||
syncButton: '同步到设备',
|
||||
cancel: '取消',
|
||||
noTargetDir: '请先选择目标文件夹。',
|
||||
noSources: '请至少选择一个来源。',
|
||||
noTracks: '所选来源中未找到曲目。',
|
||||
fetchError: '从服务器加载曲目失败。',
|
||||
syncComplete: '同步完成。',
|
||||
dismiss: '关闭',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user