feat(device-sync): overhaul Device Sync page

- New two-panel layout with live album search (search3, 300ms debounce)
  and 10 random albums when search is empty — replaces full paginated load
- Pre-sync summary modal: shows files to add/delete, net change, available
  space; Proceed button disabled when space is insufficient
- calculate_sync_payload now filters already-synced tracks (path exists on
  device) so add_bytes/add_count reflect the true delta
- Space check accounts for pending deletions: addBytes > availableBytes + delBytes
- Separate deviceSyncJobStore for ephemeral job progress state
- Removable drive detection + auto-poll every 5 s via sysinfo
- Desired-state diff: de-selecting a synced source stages it for deletion
  instead of silently removing it
- Status badges (Synced / Pending / Deletion) with matching row highlights
- Live-Search badge () and Zufallsalben section label (⇌) in album browser
- Status summary row height aligned with search field (52 px)
- i18n: all new keys added to all 8 locales (en/de/fr/nl/nb/zh/ru/es)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-14 21:37:35 +02:00
parent 915f0143f7
commit d34de09673
18 changed files with 1912 additions and 279 deletions
+40
View File
@@ -18,6 +18,7 @@ export const ruTranslation = {
expand: 'Развернуть боковую панель',
collapse: 'Свернуть боковую панель',
downloadingTracks: 'Кэширование {{n}} треков…',
syncingTracks: 'Синхронизация {{done}}/{{total}}…',
cancelDownload: 'Отменить загрузку',
offlineLibrary: 'Офлайн-библиотека',
genres: 'Жанры',
@@ -1082,8 +1083,15 @@ export const ruTranslation = {
title: 'Синхронизация устройства',
targetFolder: 'Папка назначения',
noFolderChosen: 'Папка не выбрана',
selectDrive: 'Выберите диск…',
refreshDrives: 'Обновить диски',
noDrivesDetected: 'Съёмные диски не обнаружены',
browseManual: 'Обзор вручную…',
free: 'свободно',
notMountedVolume: 'Цель не находится на смонтированном томе. Диск мог быть отключён.',
chooseFolder: 'Выбрать…',
filenameTemplate: 'Шаблон имени файла',
targetDevice: 'Целевое устройство',
templateHint: 'Переменные: {artist}, {album}, {title}, {track_number}, {disc_number}, {year}',
cleanupButton: 'Удалить файлы вне выборки',
cleanupNothingToDelete: 'Нечего удалять — устройство уже синхронизировано.',
@@ -1097,6 +1105,10 @@ export const ruTranslation = {
tabArtists: 'Исполнители',
searchPlaceholder: 'Поиск…',
syncButton: 'Синхронизировать на устройство',
actionTransfer: 'Перенести на устройство',
actionDelete: 'Удалить с устройства',
actionApplyAll: 'Применить все изменения',
templatePreview: 'Предпросмотр',
cancel: 'Отмена',
noTargetDir: 'Сначала выберите папку назначения.',
noSources: 'Выберите хотя бы один источник.',
@@ -1104,5 +1116,33 @@ export const ruTranslation = {
fetchError: 'Ошибка загрузки треков с сервера.',
syncComplete: 'Синхронизация завершена.',
dismiss: 'Закрыть',
colName: 'Название',
colType: 'Тип',
colStatus: 'Статус',
onDevice: 'Управление устройством',
addSources: 'Добавить…',
syncResult: '{{done}} перенесено, {{skipped}} уже актуально ({{total}} всего)',
deleteFromDevice: 'Пометить для удаления ({{count}})',
confirmDelete: 'Удалить {{count}} запись(ей) с устройства: {{names}}?',
deleteComplete: '{{count}} запись(ей) удалено с устройства.',
statusSynced: 'Синхронизировано',
statusPending: 'Ожидает',
statusDeletion: 'Удаление',
markForDeletion: 'Пометить для удаления',
removeSource: 'Удалить',
undoDeletion: 'Отменить удаление',
syncInBackground: 'Синхронизация запущена в фоне — можно перейти в другой раздел.',
syncInProgress: '{{done}} / {{total}} треков…',
syncSummary: 'Сводка синхронизации',
calculating: 'Вычисление необходимых данных…',
filesToAdd: 'Файлы для добавления:',
filesToDelete: 'Файлы для удаления:',
netChange: 'Чистое изменение:',
availableSpace: 'Доступное место на диске:',
spaceWarning: 'Предупреждение: на целевом устройстве недостаточно сообщаемого места.',
proceed: 'Продолжить синхронизацию',
notEnoughSpace: 'Обнаружено недостаточно физического места на диске!',
liveSearch: 'Live',
randomAlbumsLabel: 'Случайные альбомы',
},
};