diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bef3e4..6f7d3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,13 @@ ### Changed +- **Global Settings now follows the same guided structure as each library.** + Numbered, task-oriented sections give General, Connections, Notifications, + Tools, and Backup a consistent reading order, while the first-run setup + action now lives with backup and restore controls. Settings tabs expose + standard keyboard navigation, mobile actions meet touch-target guidance, and + long tool paths, statuses, and encoder names wrap inside the viewport instead + of overflowing narrow or landscape layouts. - **Scott's Settings now matches the current reference encoding bundle.** The preset still targets HEVC/MP4 at CRF 24 with AAC 96 kbps stereo, but now tone-maps HDR to SDR instead of preserving it. The global tone-map setting continues to choose compatible software or supported hardware. diff --git a/README.md b/README.md index f9c4fbb..5d142f4 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,8 @@ mounted paths, free space, filesystem/mount relationships, permissions, and medi or inaccessible storage gets concrete Docker Compose, Unraid, TrueNAS, or local recovery steps and a real Re-test action. Setup lets you fully configure as many libraries as needed and starts in **Dry-run mode**. Completing setup never starts a scan or job; review each library’s Candidates, -then scan and queue a small test set deliberately. Use **Run setup again** in the Settings header to -revisit the guided checks without deleting existing configuration. +then scan and queue a small test set deliberately. Use **Settings → Backup → First-run setup → Run +setup again** to revisit the guided checks without deleting existing configuration. Add libraries from `/data/media`; the hidden work and quarantine directories remain below the same container mount boundary. Compose examples are available for every supported runtime: diff --git a/docs/images/optimisarr-settings-backup-dark.png b/docs/images/optimisarr-settings-backup-dark.png index 55846ed..848ef4c 100644 Binary files a/docs/images/optimisarr-settings-backup-dark.png and b/docs/images/optimisarr-settings-backup-dark.png differ diff --git a/docs/images/optimisarr-settings-connections-dark.png b/docs/images/optimisarr-settings-connections-dark.png index 6fe505b..203586c 100644 Binary files a/docs/images/optimisarr-settings-connections-dark.png and b/docs/images/optimisarr-settings-connections-dark.png differ diff --git a/docs/images/optimisarr-settings-general-dark.png b/docs/images/optimisarr-settings-general-dark.png index 95b5892..f573003 100644 Binary files a/docs/images/optimisarr-settings-general-dark.png and b/docs/images/optimisarr-settings-general-dark.png differ diff --git a/docs/images/optimisarr-settings-notifications-dark.png b/docs/images/optimisarr-settings-notifications-dark.png index 62e2e6f..8aca6b0 100644 Binary files a/docs/images/optimisarr-settings-notifications-dark.png and b/docs/images/optimisarr-settings-notifications-dark.png differ diff --git a/docs/images/optimisarr-settings-tools-dark.png b/docs/images/optimisarr-settings-tools-dark.png index ea5a336..94a890a 100644 Binary files a/docs/images/optimisarr-settings-tools-dark.png and b/docs/images/optimisarr-settings-tools-dark.png differ diff --git a/docs/roadmap.md b/docs/roadmap.md index fb5a67c..5933796 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -174,7 +174,8 @@ the replacement workflow is trustworthy. a genuinely new database from an upgrade, persists each completed step, resumes after refresh or restart, accepts duplicate progress writes idempotently, and permits completion only from final review. Upgraded installations are marked complete and never forced into onboarding. Back retains - applied choices, and the Settings header offers **Run setup again** without deleting configuration. + applied choices, and **Settings → Backup → First-run setup** offers **Run setup again** without + deleting configuration. Connections remain explicitly skipped on the final review and can be added later, so an optional provider can never block first use. - **Five stable, task-oriented steps: done.** One heading and primary action drive: diff --git a/docs/setup/configuration.md b/docs/setup/configuration.md index aec1b4e..0a0ca0a 100644 --- a/docs/setup/configuration.md +++ b/docs/setup/configuration.md @@ -24,9 +24,10 @@ replace, or delete a file. Fresh installations start in dry-run with one concurrent job. Every new library has automatic enqueue, automatic replacement, and VMAF disabled unless changed explicitly in its editor. Existing installations upgraded from an older release never see the wizard automatically. To revisit it -without deleting or resetting any configuration, use **Run setup again** in the Settings header. +without deleting or resetting any configuration, use **Settings → Backup → First-run setup → Run +setup again**. -![Settings General tab showing queue, encoder, scan interval, disk threshold, and hardware decode controls](../images/optimisarr-settings-general-dark.png) +![Settings General tab grouping queue controls separately from replacement and cleanup policy](../images/optimisarr-settings-general-dark.png) ## Admin token @@ -324,7 +325,7 @@ includes libraries, activity watchers, notification targets, Arr connections, and provider credentials in plain text. Store it as sensitive material: do not commit, share, or leave it in an unprotected download directory. -![Backup tab showing export and import controls plus the sensitive-data warning](../images/optimisarr-settings-backup-dark.png) +![Backup tab separating configuration export and import from the first-run setup action](../images/optimisarr-settings-backup-dark.png) Import validates the complete file before writing, then merges configuration without deleting existing entries. It intentionally does not include media, diff --git a/web/e2e/notifications.spec.ts b/web/e2e/notifications.spec.ts index 96a4ed9..dcbeb47 100644 --- a/web/e2e/notifications.spec.ts +++ b/web/e2e/notifications.spec.ts @@ -51,7 +51,7 @@ async function mockSettings(page: Page, targets: unknown[]) { async function openNotifications(page: Page) { await page.goto('/#/settings') - await page.getByRole('button', { name: 'Notifications' }).click() + await page.getByRole('tab', { name: 'Notifications' }).click() } test('notification validation shows the provider-specific API reason', async ({ page }) => { diff --git a/web/e2e/settings.spec.ts b/web/e2e/settings.spec.ts new file mode 100644 index 0000000..9e8b37a --- /dev/null +++ b/web/e2e/settings.spec.ts @@ -0,0 +1,163 @@ +import { expect, test, type Page, type Route } from '@playwright/test' + +const settings = { + maxConcurrentJobs: 1, + minFreeDiskBytes: 10_737_418_240, + cpuThreadLimit: 0, + libraryScanIntervalHours: 1, + encoderMode: 'Auto', + hardwareDecode: true, + hdrToneMapMode: 'Software', + replacementAllowCrossFilesystem: false, + dryRunMode: true, + replacementQuarantineRetentionDays: 14, +} + +const tools = [ + { + name: 'FFmpeg', + command: '/usr/lib/jellyfin-ffmpeg/ffmpeg', + available: true, + required: true, + version: 'ffmpeg version 7.1.4-Jellyfin Copyright (c) 2000-2026 the FFmpeg developers', + error: null, + }, + { + name: 'FFmpeg (VMAF)', + command: '/usr/local/lib/optimisarr/ffmpeg-vmaf', + available: true, + required: false, + version: 'libvmaf filter available', + error: null, + }, + { + name: 'ffprobe', + command: '/usr/lib/jellyfin-ffmpeg/ffprobe', + available: true, + required: true, + version: 'ffprobe version 7.1.4-Jellyfin Copyright (c) 2007-2026 the FFmpeg developers', + error: null, + }, +] + +const hardware = { + hardwareAccelerators: ['cuda', 'vaapi', 'qsv', 'drm', 'opencl', 'vulkan'], + encoders: [ + { name: 'h264_qsv', codec: 'h264', mode: 'Intel QSV', available: true }, + { name: 'hevc_qsv', codec: 'hevc', mode: 'Intel QSV', available: true }, + { name: 'av1_qsv', codec: 'av1', mode: 'Intel QSV', available: false }, + ], + nvidiaRuntimeAvailable: false, + driDeviceAvailable: true, + error: null, +} + +function json(route: Route, body: unknown, status = 200) { + return route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) }) +} + +async function mockSettings(page: Page) { + await page.route('**/api/**', async (route) => { + const path = new URL(route.request().url()).pathname + if (path === '/api/auth/status') return json(route, { required: false }) + if (path === '/api/setup') return json(route, { + version: 1, completedStep: 5, currentStep: 5, stepCount: 5, completed: true, + }) + if (path === '/api/health') return json(route, { status: 'healthy', service: 'optimisarr', version: 'test' }) + if (path === '/api/settings') return json(route, settings) + if (path === '/api/settings/cleanup') return json(route, { + retentionDays: 14, dryRunMode: true, failedOutputCount: 2, failedOutputBytes: 2_147_483_648, + quarantinedOriginalCount: 1, quarantinedOriginalBytes: 4_294_967_296, + planToken: 'test', totalCount: 3, totalBytes: 6_442_450_944, + }) + if (path === '/api/activity-watchers' || path === '/api/arr-connections' || path === '/api/notification-targets') { + return json(route, []) + } + if (path === '/api/system/tools') return json(route, { tools }) + if (path === '/api/system/hardware') return json(route, { hardware }) + return json(route, {}) + }) +} + +test('global settings use the same logical section flow as library configuration', async ({ page }) => { + await mockSettings(page) + await page.goto('/#/settings') + + const expectedSections = new Map([ + ['General', ['Queue', 'Replacement and cleanup']], + ['Connections', ['Media servers', 'Download managers']], + ['Notifications', ['Notifications']], + ['Tools', ['Tools', 'Hardware acceleration', 'Encoders']], + ['Backup', ['Backup & restore', 'First-run setup']], + ]) + + for (const [tab, headings] of expectedSections) { + await page.getByRole('tab', { name: tab }).click() + const sections = page.locator('[role="tabpanel"] [data-config-section]') + await expect(sections.getByRole('heading', { level: 2 })).toHaveText(headings) + } + + await expect(page.getByRole('button', { name: 'Run setup again' })).toBeVisible() + + await page.getByRole('tab', { name: 'General' }).click() + await page.keyboard.press('ArrowRight') + await expect(page.getByRole('tab', { name: 'Connections' })).toHaveAttribute('aria-selected', 'true') + await expect(page.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'settings-tab-connections') +}) + +test('settings and tool capability cards stay within a small mobile viewport', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }) + await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' }) + await mockSettings(page) + await page.goto('/#/settings') + await page.locator('html').evaluate((element) => { + element.style.fontSize = '125%' + }) + const toolsTab = page.getByRole('tab', { name: 'Tools' }) + await toolsTab.click() + await expect(toolsTab).toBeInViewport() + + const fit = await page.locator('main').evaluate((main) => ({ + scrollWidth: main.scrollWidth, + clientWidth: main.clientWidth, + })) + expect(fit.scrollWidth).toBeLessThanOrEqual(fit.clientWidth) + + const cards = page.locator('[data-tool-card]') + await expect(cards).toHaveCount(3) + for (const card of await cards.all()) { + const cardFit = await card.evaluate((element) => { + const cardRect = element.getBoundingClientRect() + const mainRect = element.closest('main')!.getBoundingClientRect() + return { + scrollWidth: element.scrollWidth, + clientWidth: element.clientWidth, + left: cardRect.left, + right: cardRect.right, + mainLeft: mainRect.left, + mainRight: mainRect.right, + } + }) + expect(cardFit.scrollWidth).toBeLessThanOrEqual(cardFit.clientWidth) + expect(cardFit.left).toBeGreaterThanOrEqual(cardFit.mainLeft) + expect(cardFit.right).toBeLessThanOrEqual(cardFit.mainRight) + } + + const refreshBox = await page.getByRole('button', { name: 'Refresh' }).boundingBox() + expect(refreshBox?.height).toBeGreaterThanOrEqual(44) +}) + +test('every settings tab reflows without horizontal page overflow', async ({ page }) => { + await page.setViewportSize({ width: 812, height: 375 }) + await mockSettings(page) + await page.goto('/#/settings') + + for (const tab of ['General', 'Connections', 'Notifications', 'Tools', 'Backup']) { + await page.getByRole('tab', { name: tab }).click() + const fit = await page.locator('main').evaluate((main) => ({ + scrollWidth: main.scrollWidth, + clientWidth: main.clientWidth, + })) + expect(fit.scrollWidth).toBeLessThanOrEqual(fit.clientWidth) + } +}) diff --git a/web/src/lib/components/ToolsPanel.svelte b/web/src/lib/components/ToolsPanel.svelte index f520c93..70359ff 100644 --- a/web/src/lib/components/ToolsPanel.svelte +++ b/web/src/lib/components/ToolsPanel.svelte @@ -4,6 +4,7 @@ // tab (or anywhere) without the host wiring anything up. import { api, type EncoderCapability, type HardwareCapability, type ToolCheck } from '../api' import Banner from './Banner.svelte' + import ConfigSection from './ConfigSection.svelte' import { i18n } from '../i18n/i18n.svelte' let tools = $state([]) @@ -42,98 +43,119 @@ } -
-

- {i18n.m.shared.tools_intro} -

- -
- {#if error} {error} {/if} -
- {#each tools as tool} -
-
-
- {tool.name} - {#if !tool.required}{i18n.m.settings.optional}{/if} - {tool.command} -
- - {tool.available ? i18n.m.shared.available : i18n.m.shared.missing} - -
-
- {tool.version ?? tool.error ?? ''} -
+
+ +
+
- {/each} -
-{#if hardware} -
-

{i18n.m.shared.hardware_acceleration}

- {#if hardware.error} -
{hardware.error}
- {/if} -
-
-
{i18n.m.shared.ffmpeg_hwaccels}
-
- {#each hardware.hardwareAccelerators as accelerator} - {accelerator} - {:else} - {i18n.m.shared.none_reported} - {/each} -
-
+
+ {#each tools as tool} +
+
+
+
+

{tool.name}

+ {#if !tool.required}{i18n.m.settings.optional}{/if} +
+ {tool.command} +
+ + {tool.available ? i18n.m.shared.available : i18n.m.shared.missing} + +
+

+ {tool.version ?? tool.error ?? ''} +

+
+ {/each} +
+ -
-
{i18n.m.shared.nvidia_runtime}
- - {hardware.nvidiaRuntimeAvailable ? i18n.m.shared.available : i18n.m.shared.not_detected} - -
+ {#if hardware} + + {#if hardware.error} +
{hardware.error}
+ {/if} +
+
+

{i18n.m.shared.ffmpeg_hwaccels}

+
+ {#each hardware.hardwareAccelerators as accelerator} + {accelerator} + {:else} + {i18n.m.shared.none_reported} + {/each} +
+
+ +
+

{i18n.m.shared.nvidia_runtime}

+ + {hardware.nvidiaRuntimeAvailable ? i18n.m.shared.available : i18n.m.shared.not_detected} + +
-
-
{i18n.m.shared.dri_device}
- - {hardware.driDeviceAvailable ? i18n.m.shared.mapped : i18n.m.shared.not_mapped} - +
+

{i18n.m.shared.dri_device}

+ + {hardware.driDeviceAvailable ? i18n.m.shared.mapped : i18n.m.shared.not_mapped} + +
-
-
+ -
-

{i18n.m.shared.encoders}

-
- {#each encoderGroups as [mode, encoders]} -
-
{mode}
-
- {#each encoders as encoder} -
-
{encoder.name}
-
- {encoder.codec} - - {encoder.available ? i18n.m.shared.available : i18n.m.shared.missing} - + +
+ {#each encoderGroups as [mode, encoders]} +
+

{mode}

+
+ {#each encoders as encoder} +
+
{encoder.name}
+
+ {encoder.codec} + + {encoder.available ? i18n.m.shared.available : i18n.m.shared.missing} + +
-
- {/each} -
-
- {/each} -
-
-{/if} + {/each} +
+ + {/each} +
+ + {/if} + diff --git a/web/src/lib/i18n/de.ts b/web/src/lib/i18n/de.ts index c8139d3..ac5db2d 100644 --- a/web/src/lib/i18n/de.ts +++ b/web/src/lib/i18n/de.ts @@ -673,6 +673,7 @@ export const de: Messages = { save_settings: 'Einstellungen speichern', save_note: 'Speichert alle Optionen auf diesem Tab. Verbindungen und Benachrichtigungen werden separat gespeichert.', media_servers: 'Medienserver', + media_servers_summary: 'Neue Aufträge während der Wiedergabe pausieren und verbundene Bibliotheken nach einem geprüften Austausch aktualisieren.', media_servers_desc: 'Verbinden Sie Plex, Jellyfin oder Emby. Während ein aktivierter Server streamt, pausieren neue Aufgaben, damit Transkodierungen nie mit der Wiedergabe konkurrieren (laufende Aufgaben werden nie unterbrochen, und ein nicht erreichbarer Server pausiert die Warteschlange nie). Verbundene Server werden außerdem gebeten, einen Titel nach einer geprüften Ersetzung erneut zu scannen.', badge_refresh: 'aktualisieren', @@ -701,6 +702,7 @@ export const de: Messages = { test_connection: 'Verbindung testen', test_connection_title: 'Prüfen, ob URL und Token den Server erreichen', download_managers: 'Download-Manager', + download_managers_summary: 'Dateien aus der Warteschlange halten, während Sonarr oder Radarr sie importiert.', download_managers_desc: 'Verbinden Sie Sonarr oder Radarr. Während einer in den Ordner eines Titels importiert, werden die dortigen Dateien vom Einreihen zurückgehalten, damit eine Transkodierung nie mit einem Import konkurriert; sie werden beim nächsten Einreihen wieder geeignet, sobald der Import abgeschlossen ist. Ein nicht erreichbarer Manager blockiert die Warteschlange nie.', badge_no_key: 'kein Schlüssel', @@ -736,6 +738,7 @@ export const de: Messages = { notification_test_failed: 'Die Testbenachrichtigung konnte nicht gesendet werden.', add_target_btn: 'Ziel hinzufügen', backup_title: 'Sicherung & Wiederherstellung', + backup_summary: 'Optimisarr-Konfiguration exportieren oder zusammenführen, ohne Medien oder Auftragsverlauf zu ändern.', backup_desc: 'Exportieren Sie Ihre Einstellungen, Bibliotheken, Medienserver- und Download-Manager-Verbindungen sowie Benachrichtigungsziele in eine JSON-Datei oder importieren Sie eine. Die Datei enthält Anbieter-Token und API-Schlüssel, bewahren Sie sie also sicher auf und committen oder teilen Sie sie niemals. Sie enthält keine Medien, Aufgaben, Ersetzungen, Quarantäne oder Rücknahme-Verlauf. Der Import wird in Ihre aktuelle Konfiguration eingefügt (Bibliotheken werden anhand des Pfads, Verbindungen/Ziele anhand des Namens zugeordnet) und löscht niemals etwas.', export_config: 'Konfiguration exportieren', @@ -1064,11 +1067,13 @@ export const de: Messages = { tools_load_error: 'Werkzeuge konnten nicht geladen werden', tools_intro: 'FFmpeg und ffprobe müssen verfügbar sein, bevor Prüfung und Transkodierung ausgeführt werden können.', hardware_acceleration: 'Hardwarebeschleunigung', + hardware_acceleration_desc: 'Für FFmpeg auf diesem Host erkannte Beschleunigungs-APIs und Laufzeitgeräte.', ffmpeg_hwaccels: 'FFmpeg-Hardwarebeschleuniger', none_reported: 'Keine gemeldet', nvidia_runtime: 'NVIDIA-Laufzeit', dri_device: 'DRI-Gerät', encoders: 'Encoder', + encoders_desc: 'Prüfungen pro Codec zeigen, welche CPU- und GPU-Encoding-Pfade diese Installation verwenden kann.', all_count: 'Alle ({count})', eligible_count: 'Geeignet ({count})', skipped_count: 'Übersprungen ({count})', diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index cb03ad6..7a62bb4 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -810,6 +810,7 @@ export const en = { save_settings: 'Save settings', save_note: 'Saves every option on this tab. Connections and notifications save on their own.', media_servers: 'Media servers', + media_servers_summary: 'Pause new jobs during playback and refresh connected libraries after a verified replacement.', media_servers_desc: 'Connect Plex, Jellyfin, or Emby. While an enabled server is streaming, new jobs pause so transcodes never compete with playback (running jobs are never interrupted, and an unreachable server never pauses the queue). Connected servers are also asked to re-scan a title after a verified replacement.', badge_refresh: 'refresh', @@ -838,6 +839,7 @@ export const en = { test_connection: 'Test connection', test_connection_title: 'Check the URL and token reach the server', download_managers: 'Download managers', + download_managers_summary: 'Keep files out of the queue while Sonarr or Radarr is importing them.', download_managers_desc: "Connect Sonarr or Radarr. While one is importing into a title's folder, files there are held back from queueing so a transcode never fights an import; they become eligible again on the next enqueue once the import settles. An unreachable manager never blocks the queue.", badge_no_key: 'no key', @@ -873,6 +875,7 @@ export const en = { notification_test_failed: 'The test notification could not be sent.', add_target_btn: 'Add target', backup_title: 'Backup & restore', + backup_summary: 'Export or merge Optimisarr configuration without changing media or job history.', backup_desc: 'Export your settings, libraries, media-server and download-manager connections, and notification targets to a JSON file, or import one. The file includes provider tokens and API keys, so store it securely and never commit or share it. It does not include media, jobs, replacements, quarantine, or rollback history. Importing merges into your current config (matching libraries by path and connections/targets by name) and never deletes anything.', export_config: 'Export config', @@ -1203,11 +1206,13 @@ export const en = { tools_load_error: 'Unable to load tools', tools_intro: 'FFmpeg and ffprobe must be available before probing and transcoding can run.', hardware_acceleration: 'Hardware acceleration', + hardware_acceleration_desc: 'Acceleration APIs and runtime devices detected for FFmpeg on this host.', ffmpeg_hwaccels: 'FFmpeg hwaccels', none_reported: 'None reported', nvidia_runtime: 'NVIDIA runtime', dri_device: 'DRI device', encoders: 'Encoders', + encoders_desc: 'Per-codec checks show which CPU and GPU encoding paths this installation can use.', all_count: 'All ({count})', eligible_count: 'Eligible ({count})', skipped_count: 'Skipped ({count})', diff --git a/web/src/lib/i18n/es.ts b/web/src/lib/i18n/es.ts index bc8c795..533a674 100644 --- a/web/src/lib/i18n/es.ts +++ b/web/src/lib/i18n/es.ts @@ -669,6 +669,7 @@ true_peak_tip: save_settings: 'Guardar ajustes', save_note: 'Guarda todas las opciones en esta pestaña. Conexiones y notificaciones se guardan por separado.', media_servers: 'Servidores multimedia', + media_servers_summary: 'Pausa nuevos trabajos durante la reproducción y actualiza las bibliotecas conectadas tras un reemplazo verificado.', media_servers_desc: 'Conecta Plex, Jellyfin o Emby. Mientras un servidor habilitado está transmitiendo, los nuevos trabajos se pausan para que las transcodificaciones no compitan con la reproducción (los trabajos en curso nunca se interrumpen y un servidor inalcanzable nunca pausa la cola). También se le pide a los servidores conectados que vuelvan a escanear un título después de un reemplazo verificado.', badge_refresh: 'actualizar', @@ -697,6 +698,7 @@ true_peak_tip: test_connection: 'Probar conexión', test_connection_title: 'Verifica que la URL y el token lleguen al servidor', download_managers: 'Gestores de descargas', + download_managers_summary: 'Mantén los archivos fuera de la cola mientras Sonarr o Radarr los importa.', download_managers_desc: "Conecta Sonarr o Radarr. Mientras uno importa en la carpeta de un título, los archivos allí se retienen para que una transcodificación nunca compita con una importación; vuelven a ser elegibles en la siguiente cola cuando la importación finaliza. Un gestor inalcanzable nunca bloquea la cola.", badge_no_key: 'sin clave', @@ -732,6 +734,7 @@ true_peak_tip: notification_test_failed: 'No se pudo enviar la notificación de prueba.', add_target_btn: 'Añadir destino', backup_title: 'Copia de seguridad y restauración', + backup_summary: 'Exporta o combina la configuración de Optimisarr sin cambiar los medios ni el historial de trabajos.', backup_desc: 'Exporta tus ajustes, bibliotecas, conexiones de media server y gestor de descargas, y objetivos de notificación a un archivo JSON, o importa uno. El archivo incluye tokens de proveedores y claves API, así que guárdalo de forma segura y nunca lo compartas ni lo envíes. No incluye medios, trabajos, reemplazos, cuarentena ni historial de reversión. Importar fusiona con tu configuración actual (emparejando bibliotecas por ruta y conexiones/objetivos por nombre) y nunca elimina nada.', export_config: 'Exportar configuración', @@ -1059,11 +1062,13 @@ remove: 'Eliminar', tools_load_error: 'No se pueden cargar las herramientas', tools_intro: 'FFmpeg y ffprobe deben estar disponibles antes de que se puedan realizar el sondeo y la transcodificación.', hardware_acceleration: 'Aceleración de hardware', + hardware_acceleration_desc: 'API de aceleración y dispositivos de ejecución detectados para FFmpeg en este host.', ffmpeg_hwaccels: 'FFmpeg hwaccels', none_reported: 'Ninguna reportada', nvidia_runtime: 'Tiempo de ejecución NVIDIA', dri_device: 'Dispositivo DRI', encoders: 'Codificadores', + encoders_desc: 'Las comprobaciones por códec muestran qué rutas de codificación de CPU y GPU puede usar esta instalación.', all_count: 'Todos ({count})', eligible_count: 'Elegibles ({count})', skipped_count: 'Omitidos ({count})', diff --git a/web/src/lib/i18n/fr.ts b/web/src/lib/i18n/fr.ts index de63326..991a0b4 100644 --- a/web/src/lib/i18n/fr.ts +++ b/web/src/lib/i18n/fr.ts @@ -666,6 +666,7 @@ days: 'jours', save_settings: 'Enregistrer les paramètres', save_note: 'Enregistre toutes les options de cet onglet. Connexions et notifications s’enregistrent séparément.', media_servers: 'Serveurs multimédia', +media_servers_summary: 'Suspend les nouvelles tâches pendant la lecture et actualise les bibliothèques connectées après un remplacement vérifié.', media_servers_desc: 'Connectez Plex, Jellyfin ou Emby. Pendant qu’un serveur activé diffuse, les nouvelles tâches sont suspendues pour que les transcodages ne gênent jamais la lecture (les tâches en cours ne sont jamais interrompues et un serveur inaccessible ne met jamais la file d’attente en pause). Les serveurs connectés sont également invités à re-scanner un titre après un remplacement vérifié.', badge_refresh: 'rafraîchir', @@ -694,6 +695,7 @@ testing: 'Test en cours…', test_connection: 'Tester la connexion', test_connection_title: 'Vérifiez que l’URL et le jeton permettent d’atteindre le serveur', download_managers: 'Gestionnaires de téléchargement', +download_managers_summary: 'Écarte les fichiers de la file pendant leur importation par Sonarr ou Radarr.', download_managers_desc: "Connectez Sonarr ou Radarr. Pendant qu’un titre est en importation, les fichiers de son dossier sont retenus pour qu’un transcodage ne gêne jamais un import ; ils redeviennent éligibles au prochain ajout lorsque l’importation est terminée. Un gestionnaire inaccessible ne bloque jamais la file d’attente.", badge_no_key: 'pas de clé', @@ -729,6 +731,7 @@ notification_test_success: 'Notification de test envoyée.', notification_test_failed: 'La notification de test n’a pas pu être envoyée.', add_target_btn: 'Ajouter une cible', backup_title: 'Sauvegarde et restauration', + backup_summary: 'Exporte ou fusionne la configuration d’Optimisarr sans modifier les médias ni l’historique des tâches.', backup_desc: 'Exportez vos paramètres, médiathèques, connexions aux serveurs multimédia et aux gestionnaires de téléchargements, ainsi que les cibles de notification vers un fichier JSON, ou importez-en un. Le fichier inclut les jetons des fournisseurs et les clés API ; stockez-le en toute sécurité et ne le partagez jamais ni ne le versionnez. Il n’inclut ni médias, ni tâches, ni remplacements, ni quarantaine ou historique de restauration. L’importation fusionne avec votre configuration actuelle (en associant les médiathèques par chemin et les connexions/cibles par nom) et ne supprime jamais rien.', export_config: 'Exporter la configuration', @@ -1056,11 +1059,13 @@ backup_title: 'Sauvegarde et restauration', tools_load_error: 'Impossible de charger les outils', tools_intro: 'FFmpeg et ffprobe doivent être disponibles avant de pouvoir effectuer l’analyse et le transcodage.', hardware_acceleration: 'Accélération matérielle', + hardware_acceleration_desc: 'API d’accélération et périphériques d’exécution détectés pour FFmpeg sur cet hôte.', ffmpeg_hwaccels: 'FFmpeg hwaccels', none_reported: 'Aucun signalé', nvidia_runtime: 'Runtime NVIDIA', dri_device: 'Périphérique DRI', encoders: 'Encodeurs', + encoders_desc: 'Les vérifications par codec indiquent les chemins d’encodage CPU et GPU utilisables par cette installation.', all_count: 'Tous ({count})', eligible_count: 'Éligible ({count})', skipped_count: 'Ignoré ({count})', diff --git a/web/src/lib/i18n/it.ts b/web/src/lib/i18n/it.ts index dff11c0..3a9ae1b 100644 --- a/web/src/lib/i18n/it.ts +++ b/web/src/lib/i18n/it.ts @@ -664,6 +664,7 @@ quarantine: { save_settings: 'Salva impostazioni', save_note: 'Salva tutte le opzioni di questa scheda. Connessioni e notifiche si salvano separatamente.', media_servers: 'Media server', + media_servers_summary: 'Sospende i nuovi lavori durante la riproduzione e aggiorna le librerie collegate dopo una sostituzione verificata.', media_servers_desc: 'Collega Plex, Jellyfin o Emby. Mentre un server abilitato sta trasmettendo, i nuovi lavori vengono messi in pausa così le transcodifiche non disturbano la riproduzione (i lavori in corso non vengono mai interrotti e un server irraggiungibile non mette mai in pausa la coda). Ai server collegati viene anche richiesto di ri-scansionare un titolo dopo una sostituzione verificata.', badge_refresh: 'aggiorna', @@ -692,6 +693,7 @@ quarantine: { test_connection: 'Test connessione', test_connection_title: 'Controlla che URL e token raggiungano il server', download_managers: 'Gestori download', + download_managers_summary: 'Tiene i file fuori dalla coda mentre Sonarr o Radarr li sta importando.', download_managers_desc: "Collega Sonarr o Radarr. Mentre uno sta importando nella cartella di un titolo, i file lì vengono bloccati dalla coda così la transcodifica non interferisce con l’importazione; diventano nuovamente eleggibili al prossimo inserimento una volta conclusa l’importazione. Un gestore irraggiungibile non blocca mai la coda.", badge_no_key: 'nessuna chiave', @@ -727,6 +729,7 @@ quarantine: { notification_test_failed: 'Impossibile inviare la notifica di test.', add_target_btn: 'Aggiungi target', backup_title: 'Backup e ripristino', + backup_summary: 'Esporta o unisce la configurazione di Optimisarr senza modificare media o cronologia dei lavori.', backup_desc: 'Esporta le tue impostazioni, librerie, connessioni ai media server e gestori di download, e i target di notifiche in un file JSON, oppure importa uno. Il file include token dei provider e chiavi API: conservalo con cura e non condividerlo mai. Non include media, lavori, sostituzioni, quarantena o cronologia dei rollback. L’importazione integra la configurazione attuale (abbinando le librerie per percorso e connessioni/target per nome) e non elimina mai nulla.', export_config: 'Esporta configurazione', @@ -1054,11 +1057,13 @@ remove: 'Rimuovi', tools_load_error: 'Impossibile caricare gli strumenti', tools_intro: 'FFmpeg e ffprobe devono essere disponibili per eseguire analisi e transcodifica.', hardware_acceleration: 'Accelerazione hardware', + hardware_acceleration_desc: 'API di accelerazione e dispositivi runtime rilevati per FFmpeg su questo host.', ffmpeg_hwaccels: 'FFmpeg hwaccels', none_reported: 'Nessuna segnalata', nvidia_runtime: 'Runtime NVIDIA', dri_device: 'Dispositivo DRI', encoders: 'Encoder', + encoders_desc: 'I controlli per codec mostrano quali percorsi di codifica CPU e GPU può usare questa installazione.', all_count: 'Tutti ({count})', eligible_count: 'Idonei ({count})', skipped_count: 'Ignorati ({count})', diff --git a/web/src/lib/i18n/ja.ts b/web/src/lib/i18n/ja.ts index c7bf242..c89c43f 100644 --- a/web/src/lib/i18n/ja.ts +++ b/web/src/lib/i18n/ja.ts @@ -627,6 +627,7 @@ export const ja: Messages = { save_settings: "設定を保存する", save_note: "このタブのすべてのオプションを保存します。接続と通知は自動的に保存されます。", media_servers: "メディアサーバー", + media_servers_summary: "再生中は新しいジョブを停止し、検証済みの置換後に接続済みライブラリを更新します。", media_servers_desc: "Plex、Jellyfin、または Emby を接続します。有効なサーバーがストリーミングしている間は、新しい処理タスクが一時停止するため、トランスコードが再生と競合することはありません (実行中の処理タスクが中断されることはなく、到達不能なサーバーによってキューが一時停止されることもありません)。接続されたサーバーは、置換が確認された後にタイトルを再スキャンするように求められます。", badge_refresh: "リフレッシュする", badge_refresh_title: "交換を確認した後、このサーバーを再スキャンします。", @@ -654,6 +655,7 @@ export const ja: Messages = { test_connection: "テスト接続", test_connection_title: "URL とトークンがサーバーに到達していることを確認してください", download_managers: "ダウンロードマネージャー", + download_managers_summary: "Sonarr または Radarr がインポート中のファイルをキューから除外します。", download_managers_desc: "Sonarr または Radarr を接続します。タイトルのフォルダーにインポートしている間、そこにあるファイルはキューに入れられないため、トランスコードがインポートと競合することはありません。インポートが完了すると、次のエンキューで再び対象になります。到達不能なマネージャーがキューをブロックすることはありません。", badge_no_key: "鍵がありません", badge_no_key_title: "API キーが設定されていません — Optimisarr はこのマネージャーにクエリを実行できません。", @@ -687,6 +689,7 @@ export const ja: Messages = { notification_test_failed: "テスト通知を送信できませんでした。", add_target_btn: "ターゲットの追加", backup_title: "バックアップと復元", + backup_summary: "メディアやジョブ履歴を変更せずに Optimisarr の設定をエクスポートまたは統合します。", backup_desc: "設定、メディア ライブラリ、メディア サーバーとダウンロード マネージャーの接続、通知ターゲットを JSON ファイルにエクスポートするか、インポートします。ファイルにはプロバイダー トークンと API キーが含まれているため、安全に保存し、決してコミットしたり共有したりしないでください。これには、メディア、処理タスク、交換、隔離、またはロールバック履歴は含まれません。インポートすると、現在の構成にマージされ (メディア ライブラリをパスで、接続/ターゲットを名前で照合)、何も削除されません。", export_config: "設定のエクスポート", importing: "インポート中…", @@ -1011,11 +1014,13 @@ export const ja: Messages = { tools_load_error: "ツールをロードできません", tools_intro: "プローブとトランスコーディングを実行するには、FFmpeg と ffprobe が使用可能になっている必要があります。", hardware_acceleration: "ハードウェアアクセラレーション", + hardware_acceleration_desc: "このホストで FFmpeg が検出したアクセラレーション API とランタイムデバイス。", ffmpeg_hwaccels: "FFmpeg ワッセル", none_reported: "報告なし", nvidia_runtime: "NVIDIA ランタイム", dri_device: "DRI デバイス", encoders: "エンコーダ", + encoders_desc: "コーデックごとのチェックで、このインストールが使用できる CPU と GPU のエンコード経路を示します。", all_count: "すべて ({count})", eligible_count: "対象 ({count})", skipped_count: "スキップされました ({count})", diff --git a/web/src/lib/i18n/pt.ts b/web/src/lib/i18n/pt.ts index 55f810c..215c37a 100644 --- a/web/src/lib/i18n/pt.ts +++ b/web/src/lib/i18n/pt.ts @@ -627,6 +627,7 @@ export const pt: Messages = { save_settings: "Salvar configurações", save_note: "Salva todas as opções nesta guia. Conexões e notificações são salvas por conta própria.", media_servers: "Servidores de mídia", + media_servers_summary: "Pausa novos trabalhos durante a reprodução e atualiza bibliotecas conectadas após uma substituição verificada.", media_servers_desc: "Conecte Plex, Jellyfin ou Emby. Enquanto um servidor habilitado está transmitindo, novas tarefas de processamento são pausadas para que as transcodificações nunca concorram com a reprodução (as tarefas de processamento em execução nunca são interrompidas e um servidor inacessível nunca pausa a fila). Os servidores conectados também são solicitados a digitalizar novamente um título após uma substituição verificada.", badge_refresh: "atualizar", badge_refresh_title: "Verifica novamente este servidor após uma substituição verificada.", @@ -654,6 +655,7 @@ export const pt: Messages = { test_connection: "Conexão de teste", test_connection_title: "Verifique se o URL e o token chegam ao servidor", download_managers: "Gerenciadores de download", + download_managers_summary: "Mantém os arquivos fora da fila enquanto o Sonarr ou o Radarr os importa.", download_managers_desc: "Conecte Sonarr ou Radarr. Enquanto alguém está importando para a pasta de um título, os arquivos lá são impedidos de entrar na fila para que uma transcodificação nunca combata uma importação; eles se tornam elegíveis novamente no próximo enfileiramento assim que a importação for concluída. Um gerente inacessível nunca bloqueia a fila.", badge_no_key: "sem chave", badge_no_key_title: "Nenhuma chave API definida — Optimisarr não pode consultar este gerenciador.", @@ -687,6 +689,7 @@ export const pt: Messages = { notification_test_failed: "Não foi possível enviar a notificação de teste.", add_target_btn: "Adicionar destino", backup_title: "Backup e restauração", + backup_summary: "Exporta ou mescla a configuração do Optimisarr sem alterar mídia ou histórico de trabalhos.", backup_desc: "Exporte suas configurações, bibliotecas de mídia, conexões de servidor de mídia e gerenciador de download e destinos de notificação para um arquivo JSON ou importe um. O arquivo inclui tokens de provedor e chaves API, portanto, armazene-o com segurança e nunca confirme ou compartilhe-o. Não inclui mídia, tarefas de processamento, substituições, quarentena ou histórico de reversão. A importação mescla em sua configuração atual (combinando bibliotecas de mídia por caminho e conexões/destinos por nome) e nunca exclui nada.", export_config: "Exportar configuração", importing: "Importando…", @@ -1011,11 +1014,13 @@ export const pt: Messages = { tools_load_error: "Não foi possível carregar ferramentas", tools_intro: "FFmpeg e ffprobe devem estar disponíveis antes que a análise e a transcodificação possam ser executadas.", hardware_acceleration: "Aceleração de hardware", + hardware_acceleration_desc: "APIs de aceleração e dispositivos de execução detectados para o FFmpeg neste host.", ffmpeg_hwaccels: "FFmpeg hwaccels", none_reported: "Nenhum relatado", nvidia_runtime: "Tempo de execução NVIDIA", dri_device: "Dispositivo DRI", encoders: "Codificadores", + encoders_desc: "As verificações por codec mostram quais caminhos de codificação de CPU e GPU esta instalação pode usar.", all_count: "Todos ({count})", eligible_count: "Elegível ({count})", skipped_count: "Ignorado ({count})", diff --git a/web/src/lib/i18n/ru.ts b/web/src/lib/i18n/ru.ts index a7dfe70..57ae2fc 100644 --- a/web/src/lib/i18n/ru.ts +++ b/web/src/lib/i18n/ru.ts @@ -627,6 +627,7 @@ export const ru: Messages = { save_settings: "Сохранить настройки", save_note: "Сохраняет все параметры на этой вкладке. Соединения и уведомления сохраняются самостоятельно.", media_servers: "Медиа-серверы", + media_servers_summary: "Приостанавливает новые задания во время воспроизведения и обновляет подключённые библиотеки после проверенной замены.", media_servers_desc: "Подключите Plex, Jellyfin или Emby. Пока включенный сервер выполняет потоковую передачу, новые задачи обработки приостанавливаются, поэтому транскодирование никогда не конкурирует с воспроизведением (выполняющиеся задачи обработки никогда не прерываются, а недоступный сервер никогда не приостанавливает очередь). Подключенным серверам также предлагается повторно отсканировать заголовок после проверенной замены.", badge_refresh: "обновить", badge_refresh_title: "Повторно сканирует этот сервер после проверенной замены.", @@ -654,6 +655,7 @@ export const ru: Messages = { test_connection: "Тестовое соединение", test_connection_title: "Проверьте URL и токен достигнет сервера.", download_managers: "Менеджеры загрузок", + download_managers_summary: "Не добавляет файлы в очередь, пока Sonarr или Radarr импортирует их.", download_managers_desc: "Подключите Sonarr или Radarr. Пока кто-то импортирует в папку с заголовком, файлы там не ставятся в очередь, поэтому перекодирование никогда не препятствует импорту; они снова получат право на участие в следующей очереди после урегулирования импорта. Недоступный менеджер никогда не блокирует очередь.", badge_no_key: "нет ключа", badge_no_key_title: "Ключ API не установлен — Optimisarr не может запросить этот менеджер.", @@ -687,6 +689,7 @@ export const ru: Messages = { notification_test_failed: "Не удалось отправить тестовое уведомление.", add_target_btn: "Добавить цель", backup_title: "Резервное копирование и восстановление", + backup_summary: "Экспортирует или объединяет конфигурацию Optimisarr без изменения медиа и истории заданий.", backup_desc: "Экспортируйте свои настройки, медиа-библиотеки, соединения с медиа-сервером и менеджером загрузок, а также цели уведомлений в файл JSON или импортируйте его. Файл включает токены поставщика и ключи API, поэтому храните его в надежном месте, никогда не фиксируйте и не делитесь им. Он не включает носители, задачи обработки, замены, карантин или историю отката. Импорт объединяется в вашу текущую конфигурацию (сопоставляя медиа-библиотеки по пути и соединениям/целям по имени) и никогда ничего не удаляет.", export_config: "Экспортировать конфигурацию", importing: "Импорт…", @@ -1011,11 +1014,13 @@ export const ru: Messages = { tools_load_error: "Невозможно загрузить инструменты", tools_intro: "FFmpeg и ffprobe должны быть доступны, прежде чем можно будет запустить зондирование и перекодирование.", hardware_acceleration: "Аппаратное ускорение", + hardware_acceleration_desc: "API ускорения и устройства выполнения, обнаруженные для FFmpeg на этом хосте.", ffmpeg_hwaccels: "FFmpeg hwaccels", none_reported: "Ничего не сообщается", nvidia_runtime: "Среда выполнения NVIDIA", dri_device: "Устройство DRI", encoders: "Кодеры", + encoders_desc: "Проверки по кодекам показывают, какие пути кодирования CPU и GPU доступны этой установке.", all_count: "Все ({count})", eligible_count: "Соответствует критериям ({count})", skipped_count: "Пропущено ({count})", diff --git a/web/src/lib/i18n/zh.ts b/web/src/lib/i18n/zh.ts index dd8b918..06e3262 100644 --- a/web/src/lib/i18n/zh.ts +++ b/web/src/lib/i18n/zh.ts @@ -627,6 +627,7 @@ export const zh: Messages = { save_settings: "保存设置", save_note: "保存此选项卡上的每个选项。连接和通知会自行保存。", media_servers: "媒体服务器", + media_servers_summary: "播放期间暂停新任务,并在验证替换后刷新已连接的媒体库。", media_servers_desc: "连接 Plex、Jellyfin 或 Emby。当启用的服务器正在流式传输时,新的处理任务会暂停,因此转码永远不会与播放竞争(正在运行的处理任务永远不会中断,并且无法访问的服务器永远不会暂停队列)。在验证替换后,连接的服务器也会被要求重新扫描标题。", badge_refresh: "刷新", badge_refresh_title: "验证更换后重新扫描该服务器。", @@ -654,6 +655,7 @@ export const zh: Messages = { test_connection: "测试连接", test_connection_title: "检查URL和令牌是否到达服务器", download_managers: "下载管理器", + download_managers_summary: "Sonarr 或 Radarr 导入文件时,暂不将其加入队列。", download_managers_desc: "连接Sonarr或Radarr。当用户导入到标题的文件夹时,那里的文件会被阻止排队,因此转码永远不会影响导入;一旦导入完成,它们将在下一次排队时再次符合资格。无法访问的管理器永远不会阻塞队列。", badge_no_key: "没有钥匙", badge_no_key_title: "无 API 密钥集 — Optimisarr 无法查询该管理器。", @@ -687,6 +689,7 @@ export const zh: Messages = { notification_test_failed: "无法发送测试通知。", add_target_btn: "添加目标", backup_title: "备份与恢复", + backup_summary: "导出或合并 Optimisarr 配置,而不更改媒体或任务历史记录。", backup_desc: "将您的设置、媒体库、媒体服务器和下载管理器连接以及通知目标导出到 JSON 文件,或导入文件。该文件包含提供者令牌和 API 密钥,因此请安全存储它,切勿提交或共享它。它不包括媒体、处理任务、替换、隔离或回滚历史记录。导入合并到您当前的配置中(按路径匹配媒体库并按名称匹配连接/目标)并且永远不会删除任何内容。", export_config: "导出配置", importing: "输入…", @@ -1011,11 +1014,13 @@ export const zh: Messages = { tools_load_error: "无法加载工具", tools_intro: "在运行探测和转码之前,FFmpeg 和 ffprobe 必须可用。", hardware_acceleration: "硬件加速", + hardware_acceleration_desc: "此主机上的 FFmpeg 检测到的加速 API 和运行时设备。", ffmpeg_hwaccels: "FFmpeg hwaccels", none_reported: "没有报道", nvidia_runtime: "NVIDIA运行时", dri_device: "DRI装置", encoders: "编码器", + encoders_desc: "按编解码器检查此安装可使用的 CPU 和 GPU 编码路径。", all_count: "全部 ({count})", eligible_count: "符合资格 ({count})", skipped_count: "已跳过 ({count})", diff --git a/web/src/lib/pages/Settings.svelte b/web/src/lib/pages/Settings.svelte index 49d3a7b..a2dc731 100644 --- a/web/src/lib/pages/Settings.svelte +++ b/web/src/lib/pages/Settings.svelte @@ -25,6 +25,7 @@ import InfoTip from '../components/InfoTip.svelte' import Icon from '../components/Icon.svelte' import Banner from '../components/Banner.svelte' + import ConfigSection from '../components/ConfigSection.svelte' import ToolsPanel from '../components/ToolsPanel.svelte' // Settings is split into tabs so each concern is found quickly and Tools lives here @@ -41,6 +42,26 @@ // A visit to the old /tools route lands on Settings with the Tools tab open. let activeTab = $state(router.path.startsWith('/tools') ? 'tools' : 'general') + function selectTab(key: TabKey) { + activeTab = key + requestAnimationFrame(() => { + const tab = document.getElementById(`settings-tab-${key}`) + tab?.focus() + tab?.scrollIntoView({ block: 'nearest', inline: 'nearest' }) + }) + } + + function handleTabKeydown(event: KeyboardEvent, index: number) { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return + event.preventDefault() + const nextIndex = event.key === 'Home' + ? 0 + : event.key === 'End' + ? tabs.length - 1 + : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length + selectTab(tabs[nextIndex].key) + } + const notificationTypes: NotificationType[] = ['Webhook', 'Discord', 'Telegram', 'Ntfy', 'Apprise'] const emptyTarget = (): SaveNotificationTarget => ({ name: '', type: 'Webhook', url: '', token: '', enabled: true, notifyOnReplacement: true, notifyOnFailure: true, @@ -572,15 +593,11 @@ } -
+

{i18n.m.nav.settings}

{i18n.m.settings.subtitle}

-
{#if error} @@ -590,13 +607,23 @@ {#if loading}
{i18n.m.common.loading_short}
{:else} -
- {#each tabs as tab} +
+ {#each tabs as tab, index} @@ -604,12 +631,18 @@
{#if activeTab === 'general'} -
-
-

{i18n.m.nav.queue}

-

- {i18n.m.settings.queue_desc} -

+
+
@@ -669,13 +702,14 @@ {i18n.m.settings.auto_run_before}{i18n.m.settings.auto_run_after}

-
- -
-

{i18n.m.settings.replacement_title}

-

- {i18n.m.settings.replacement_desc} -

+ + +
-
+ -
- +
+ {#if message}{message}{/if} {i18n.m.settings.save_note}
@@ -750,11 +784,20 @@ {/if} {#if activeTab === 'connections'} -
+
-
-

{i18n.m.settings.media_servers}

-

+ +

{i18n.m.settings.media_servers_desc}

@@ -775,8 +818,8 @@ {#if !w.enabled}{i18n.m.settings.disabled}{/if} {#if w.refreshOnReplace}{i18n.m.settings.badge_refresh}{/if} {#if !w.hasToken}{i18n.m.settings.badge_no_token}{/if} - - + +
{/each} @@ -811,7 +854,7 @@ -
+
{#if watcherDraft.type !== 'Emby'} {#if connecting} - + {:else} - {/if} @@ -870,12 +913,12 @@ {/if}

{/if} -
- + {/if}
-
+ -
-

{i18n.m.settings.download_managers}

-

+ +

{i18n.m.settings.download_managers_desc}

@@ -912,8 +959,8 @@
{#if !c.enabled}{i18n.m.settings.disabled}{/if} {#if !c.hasApiKey}{i18n.m.settings.badge_no_key}{/if} - - + +
{/each} @@ -955,25 +1002,32 @@
-
- {#if editingArrId !== null} - + {/if}
-
+
{/if} {#if activeTab === 'notifications'} -
-

{i18n.m.settings.tab_notifications}

-

- {i18n.m.settings.notifications_desc} -

+
+ {#if targetError}
{targetError}
@@ -996,11 +1050,11 @@ {#if t.type === 'Telegram' && !t.hasToken}{i18n.m.settings.badge_no_token}{/if} {#if t.notifyOnReplacement}{i18n.m.settings.badge_replaced}{/if} {#if t.notifyOnFailure}{i18n.m.settings.badge_failed}{/if} - - - + +
{/each} @@ -1062,26 +1116,44 @@
-
- {#if editingTargetId !== null} - + {/if}
+
{/if} {#if activeTab === 'tools'} - +
+ +
{/if} {#if activeTab === 'backup'} -
-

{i18n.m.settings.backup_title}

-

+

+ +

{i18n.m.settings.backup_desc}

@@ -1100,6 +1172,19 @@
+ + + + +
{/if} {/if}