From 54a099136af5486577ffaa80bb6a842329f53a6e Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:22:30 +0200 Subject: [PATCH] fix: make folder monitoring automation explicit Honor the auto-upload setting for watched files with and without host preselection, keep queued preview jobs isolated from active batch summaries, and add localized accessible help for every folder-monitor behavior option. --- renderer/app.js | 159 ++++++++++++++++++++------------- renderer/i18n.js | 8 +- renderer/styles.css | 60 +++++++++++++ tests/startup-renderer.test.js | 149 +++++++++++++++++++++++++++++- 4 files changed, 311 insertions(+), 65 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index 4fa4d38..6c6acda 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -408,6 +408,66 @@ window.addEventListener('unhandledrejection', (e) => { } catch {} }); +function resolveFolderMonitorQueueAction({ autoStart, uploading: isUploading, healthCheckRunning: isHealthCheckRunning }) { + if (!autoStart) return 'queue'; + if (isUploading) return 'inject'; + return isHealthCheckRunning ? 'queue' : 'start'; +} + +function handleFolderMonitorFiles(files) { + window.api.debugLog('folder-monitor: received ' + files.length + ' file(s)'); + const fm = config.globalSettings && config.globalSettings.folderMonitor; + const fmHosters = fm && Array.isArray(fm.hosters) && fm.hosters.length > 0 ? fm.hosters : []; + + if (fmHosters.length === 0) { + addPathsToQueue(files, { folderMonitorAutoStart: !!fm.autoStart }); + return; + } + + selectedUploadHosters = fmHosters.slice(); + const existing = new Set(); + for (const file of selectedFiles) existing.add(file.path); + for (const file of _pendingFiles) existing.add(file.path); + const newFiles = []; + for (const filePath of files) { + if (existing.has(filePath)) continue; + existing.add(filePath); + const name = filePath.split('\\').pop().split('/').pop(); + newFiles.push({ path: filePath, name, size: null }); + } + if (newFiles.length === 0) return; + + const newPaths = new Set(newFiles.map(file => file.path)); + clearDedupKeysForPaths(newPaths); + selectedFiles.push(...newFiles); + buildQueuePreview(); + updateUploadView(); + const action = resolveFolderMonitorQueueAction({ + autoStart: fm.autoStart, + uploading, + healthCheckRunning + }); + if (action === 'start') { + startUpload(); + return; + } + if (action !== 'inject') return; + + const newJobs = queueJobs.filter(job => job.status === 'preview' && newPaths.has(job.file)); + if (newJobs.length === 0) return; + const cleanupPreparation = prepareSourceCleanup(newJobs); + newJobs.forEach(job => { job.status = 'queued'; }); + renderQueueTable(); + window.api.addJobsToBatch({ + jobs: newJobs.map(serializeUploadJob), + sourceCleanupGroups: cleanupPreparation.groups + }).then(result => { + _markSkippedJobs(result); + if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints); + }).catch(() => {}); + persistQueueStateSoon(true); +} + // --- Init --- async function init() { try { @@ -501,56 +561,7 @@ async function init() { } }); - // Folder monitor: auto-queue new files - window.api.onFolderMonitorNewFiles((files) => { - window.api.debugLog('folder-monitor: received ' + files.length + ' file(s)'); - const fm = config.globalSettings && config.globalSettings.folderMonitor; - const fmHosters = fm && Array.isArray(fm.hosters) && fm.hosters.length > 0 ? fm.hosters : []; - - if (fmHosters.length > 0) { - // Pre-selected hosters: set them as active selection and add directly to queue - selectedUploadHosters = fmHosters.slice(); - const existing = new Set(); - for (const f of selectedFiles) existing.add(f.path); - for (const f of _pendingFiles) existing.add(f.path); - const newFiles = []; - for (const p of files) { - if (existing.has(p)) continue; - existing.add(p); - const name = p.split('\\').pop().split('/').pop(); - newFiles.push({ path: p, name, size: null }); - } - if (newFiles.length > 0) { - const newPaths = new Set(newFiles.map(f => f.path)); - clearDedupKeysForPaths(newPaths); - selectedFiles.push(...newFiles); - buildQueuePreview(); - updateUploadView(); - if (fm.autoStart && !uploading && !healthCheckRunning) { - startUpload(); - } else if (uploading) { - // Inject new preview jobs into the running batch - const newJobs = queueJobs.filter(j => j.status === 'preview' && newPaths.has(j.file)); - if (newJobs.length > 0) { - const cleanupPreparation = prepareSourceCleanup(newJobs); - newJobs.forEach(j => { j.status = 'queued'; }); - renderQueueTable(); - window.api.addJobsToBatch({ - jobs: newJobs.map(serializeUploadJob), - sourceCleanupGroups: cleanupPreparation.groups - }).then(result => { - _markSkippedJobs(result); - if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints); - }).catch(() => {}); - persistQueueStateSoon(true); - } - } - } - } else { - // No pre-selected hosters: open modal - addPathsToQueue(files); - } - }); + window.api.onFolderMonitorNewFiles(handleFolderMonitorFiles); // Account switched notification window.api.onAccountSwitched((data) => { @@ -1163,6 +1174,10 @@ function applyHosterSelection() { const admittedFiles = _pendingFiles.filter(file => window.ImportPreflight .getEligibleImportHosters(file, selectedUploadHosters, hosterSettings).length > 0); const pendingPaths = new Set(admittedFiles.map(f => f.path)); + const pathsToInject = new Set(admittedFiles + .filter(file => !_pendingFolderMonitorAutoStart.has(file.path) || _pendingFolderMonitorAutoStart.get(file.path)) + .map(file => file.path)); + const shouldAutoStart = !uploading && admittedFiles.some(file => _pendingFolderMonitorAutoStart.get(file.path) === true); if (admittedFiles.length > 0) { selectedFiles.push(...admittedFiles); } @@ -1173,9 +1188,9 @@ function applyHosterSelection() { // During an active upload, build preview jobs for the new files and inject // them into the running batch immediately (otherwise they'd be lost on // handleBatchDone via syncSelectedFilesFromQueue) - if (uploading && pendingPaths.size > 0) { - buildQueuePreview(); // creates 'preview' jobs for new files - const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file)); + if (pendingPaths.size > 0) buildQueuePreview(); + if (uploading && pathsToInject.size > 0) { + const newJobs = queueJobs.filter(j => j.status === 'preview' && pathsToInject.has(j.file)); if (newJobs.length > 0) { const cleanupPreparation = prepareSourceCleanup(newJobs); newJobs.forEach(j => { j.status = 'queued'; }); @@ -1193,8 +1208,10 @@ function applyHosterSelection() { updateUploadView(); persistQueueStateSoon(true); // immediate persist after adding files + _pendingFolderMonitorAutoStart.clear(); _pendingImportInspection = null; document.getElementById('hosterModal').style.display = 'none'; + if (shouldAutoStart) startUpload(); return true; } @@ -1203,6 +1220,7 @@ function cancelHosterModal() { _pendingImportInspections = 0; _importCoordination = Promise.resolve(); _pendingFiles = []; + _pendingFolderMonitorAutoStart.clear(); _pendingImportInspection = null; syncImportConfirmationState(); closeHosterModal(); @@ -1420,6 +1438,7 @@ function setupDragDrop() { } let _pendingFiles = []; +const _pendingFolderMonitorAutoStart = new Map(); let _pendingImportInspection = null; let _importCoordination = Promise.resolve(); let _importGeneration = 0; @@ -1516,7 +1535,11 @@ function mergePendingImportInspection(result) { }; } -function coordinateImportEntries(entries) { +function markPendingFolderMonitorFiles(files, autoStart) { + for (const file of files) _pendingFolderMonitorAutoStart.set(file.path, !!autoStart); +} + +function coordinateImportEntries(entries, options = {}) { const candidates = Array.isArray(entries) ? entries : []; if (candidates.length === 0) return Promise.resolve(null); const generation = _importGeneration; @@ -1535,6 +1558,9 @@ function coordinateImportEntries(entries) { if (generation !== _importGeneration) return null; mergePendingImportInspection(inspection); if (inspection.accepted.length > 0) { + if (typeof options.folderMonitorAutoStart === 'boolean') { + markPendingFolderMonitorFiles(inspection.accepted, options.folderMonitorAutoStart); + } if (document.getElementById('hosterModal')?.style.display === 'flex') { selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked')) .map(input => input.dataset.hosterModal); @@ -1596,8 +1622,8 @@ async function pickFolder() { return enqueueImportEntries(paths); } -function addPathsToQueue(paths) { - return coordinateImportEntries(paths); +function addPathsToQueue(paths, options) { + return coordinateImportEntries(paths, options); } function updateUploadView() { @@ -4235,6 +4261,7 @@ function applySummaryResults(summary) { // otherwise become O(n²). const jobByKey = new Map(); for (const j of queueJobs) { + if (j.status === 'preview') continue; jobByKey.set(`${j.fileName}\u0001${j.hoster}`, j); } for (const file of files) { @@ -4976,6 +5003,7 @@ function renderSettings() { `; + const folderMonitorHelp = (key, text) => ``; pages.uploads.innerHTML = ` ${pageHeader('Upload-Verhalten', 'Globale Leistung, Warteschlange und Verhalten nach einem erfolgreichen Upload.')} @@ -5068,28 +5096,33 @@ function renderSettings() {
Verhalten
-
+
+ ${folderMonitorHelp('enabled', 'Startet die Überwachung nach dem Speichern, wenn ein Ordner ausgewählt ist.')}
-
+
+ ${folderMonitorHelp('recursive', 'Überwacht zusätzlich alle Unterordner des ausgewählten Ordners.')}
-
+
+ ${folderMonitorHelp('existing', 'Fügt beim nächsten Start der Überwachung alle bereits vorhandenen passenden Dateien hinzu. Die Option wird danach automatisch deaktiviert.')}
-
+
+ ${folderMonitorHelp('duplicates', 'Ignoriert wiederholte Erkennungen desselben Dateipfads während der aktuellen Überwachung.')}
-
+
+ ${folderMonitorHelp('auto-start', 'Startet neu erkannte Dateien automatisch. Ohne diese Option werden sie nur zur Warteschlange hinzugefügt.')}
- +
${configuredAccounts.map(({ name }) => `
@@ -5097,7 +5130,7 @@ function renderSettings() {
`).join('')}
- ${configuredAccounts.length === 0 ? '

Erst Accounts anlegen, dann hier auswählen.

' : '

Keine Auswahl = Hoster-Modal bei jeder Datei.

'} + ${configuredAccounts.length === 0 ? '

Erst Accounts anlegen, dann hier auswählen.

' : '

Keine Vorauswahl = manuelle Hoster-Auswahl für neu erkannte Dateien.

'} `; pages.benachrichtigungen.innerHTML = ` diff --git a/renderer/i18n.js b/renderer/i18n.js index d158b6e..7f9f52a 100644 --- a/renderer/i18n.js +++ b/renderer/i18n.js @@ -265,8 +265,14 @@ ['Duplikate überspringen', 'Skip duplicates'], ['Auto-Upload starten', 'Start uploads automatically'], ['Hoster-Vorauswahl', 'Host preselection'], + ['Startet die Überwachung nach dem Speichern, wenn ein Ordner ausgewählt ist.', 'Starts monitoring after saving when a folder is selected.'], + ['Überwacht zusätzlich alle Unterordner des ausgewählten Ordners.', 'Also monitors every subfolder inside the selected folder.'], + ['Fügt beim nächsten Start der Überwachung alle bereits vorhandenen passenden Dateien hinzu. Die Option wird danach automatisch deaktiviert.', 'Adds all matching files already present when monitoring starts next. The option is then disabled automatically.'], + ['Ignoriert wiederholte Erkennungen desselben Dateipfads während der aktuellen Überwachung.', 'Ignores repeated detections of the same file path during the current monitoring session.'], + ['Startet neu erkannte Dateien automatisch. Ohne diese Option werden sie nur zur Warteschlange hinzugefügt.', 'Starts newly detected files automatically. Without this option, they are only added to the queue.'], + ['Legt die Upload-Ziele für Dateien aus der Ordnerüberwachung fest. Ohne Auswahl ist eine manuelle Bestätigung erforderlich.', 'Sets the upload destinations for files from folder monitoring. Without a selection, manual confirmation is required.'], ['Erst Accounts anlegen, dann hier auswählen.', 'Add accounts first, then select them here.'], - ['Keine Auswahl = Hoster-Modal bei jeder Datei.', 'No selection = show the host dialog for every file.'], + ['Keine Vorauswahl = manuelle Hoster-Auswahl für neu erkannte Dateien.', 'No preselection means manually choosing hosts for newly detected files.'], ['Meldungen nach einem abgeschlossenen Upload-Batch versenden.', 'Send notifications after a completed upload batch.'], ['Webhook-Adresse', 'Webhook URL'], ['Test senden', 'Send test'], diff --git a/renderer/styles.css b/renderer/styles.css index 9869fcd..44797fb 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -1313,6 +1313,66 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } .settings-subpage .settings-grid-mini .checkbox-row label { min-width: 0; cursor: pointer; } .settings-subpage .settings-grid-mini .checkbox-row input[type="checkbox"] { order: 2; width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; } +.settings-subpage .settings-grid-mini .settings-row.checkbox-row.folder-monitor-help-row { + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 10px; +} +.settings-subpage .settings-grid-mini .folder-monitor-help-row input[type="checkbox"] { order: 3; } +.settings-help { + position: relative; + order: 2; + display: inline-grid; + place-items: center; + width: 20px; + height: 20px; + color: var(--text-dim); + cursor: help; + outline: none; +} +.settings-help svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} +.settings-help::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 8px); + right: 0; + width: max-content; + max-width: min(320px, calc(100vw - 72px)); + padding: 9px 11px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-raised); + color: var(--text); + box-shadow: var(--shadow-lg); + font-size: 11px; + font-weight: 600; + line-height: 1.45; + letter-spacing: normal; + text-align: left; + text-transform: none; + white-space: normal; + opacity: 0; + pointer-events: none; + transform: translateY(-3px); + transition: opacity .14s, transform .14s; + z-index: 90; +} +.settings-help:hover, +.settings-help:focus-visible { + color: var(--text); +} +.settings-help:hover::after, +.settings-help:focus-visible::after { + opacity: 1; + transform: translateY(0); +} .settings-hoster-pointer { margin-top: 16px; padding: 11px 13px; diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index 8aaa4df..445fbe9 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -63,6 +63,7 @@ test('Windows compositor paints the full hidden surface with an RDP session envi fs.writeFileSync(preloadPath, ` const { contextBridge } = require('electron'); const managedOnlineBackupProbeCalls = []; +const folderMonitorProbeCalls = []; const managedOnlineBackupIds = { a: 'AAAAAAAAAAAAAAAAAAAAAA', b: 'AQEBAQEBAQEBAQEBAQEBAQ', @@ -142,7 +143,17 @@ contextBridge.exposeInMainWorld('api', { pendingManagedOnlineBackupDelete = null; pending.resolve({ ok: true, removedId: pending.id, notFound: false }); }, - getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; } + getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; }, + debugLog() {}, + savePendingQueue(payload) { + folderMonitorProbeCalls.push(['save', payload?.queueJobs?.length || 0]); + return Promise.resolve(true); + }, + addJobsToBatch(payload) { + folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]); + return Promise.resolve({}); + }, + getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; } }); `, 'utf8'); const appDialogBehaviorScript = `(async () => { @@ -261,6 +272,105 @@ contextBridge.exposeInMainWorld('api', { }; return { germanState, updatePaths, exceptionalPaths, stableSectionPath, umlautAliasPath, managedBackupNavigation, navigationState, englishPath, clearedState }; })()`; + const folderMonitorBehaviorScript = `(async () => { + setUiLanguage('de'); + renderSettings(); + await new Promise(resolve => setTimeout(resolve, 0)); + const keys = ['enabled', 'recursive', 'existing', 'duplicates', 'auto-start', 'hosters']; + const readTooltips = () => Object.fromEntries(keys.map(key => { + const element = document.querySelector('[data-folder-monitor-help="' + key + '"]'); + return [key, { tooltip: element?.dataset.tooltip, label: element?.getAttribute('aria-label') }]; + })); + const germanTooltips = readTooltips(); + const gridScope = { + ordinary: document.getElementById('alwaysOnTopInput').closest('.checkbox-row').classList.contains('folder-monitor-help-row'), + folderMonitor: document.getElementById('fmEnabledInput').closest('.checkbox-row').classList.contains('folder-monitor-help-row') + }; + setUiLanguage('en'); + await new Promise(resolve => setTimeout(resolve, 0)); + const englishTooltips = readTooltips(); + const actions = { + disabledWhileIdle: resolveFolderMonitorQueueAction({ autoStart: false, uploading: false, healthCheckRunning: false }), + disabledWhileUploading: resolveFolderMonitorQueueAction({ autoStart: false, uploading: true, healthCheckRunning: false }), + enabledWhileIdle: resolveFolderMonitorQueueAction({ autoStart: true, uploading: false, healthCheckRunning: false }), + enabledWhileUploading: resolveFolderMonitorQueueAction({ autoStart: true, uploading: true, healthCheckRunning: false }), + enabledDuringHealthCheck: resolveFolderMonitorQueueAction({ autoStart: true, uploading: false, healthCheckRunning: true }) + }; + const resetQueue = autoStart => { + config = { + hosters: Object.fromEntries(HOSTERS.map(hoster => [hoster, []])), + globalSettings: { folderMonitor: { hosters: ['doodstream.com'], autoStart } } + }; + hosterSettings = {}; + selectedUploadHosters = []; + selectedFiles = []; + queueJobs = []; + uploading = true; + healthCheckRunning = false; + rebuildJobIndex(); + }; + resetQueue(false); + handleFolderMonitorFiles(['C:\\\\folder-monitor-queue-only.mkv']); + await new Promise(resolve => setTimeout(resolve, 0)); + const queueOnly = { + statuses: queueJobs.map(job => job.status), + injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length + }; + resetQueue(true); + handleFolderMonitorFiles(['C:\\\\folder-monitor-inject.mkv']); + await new Promise(resolve => setTimeout(resolve, 0)); + const autoStart = { + statuses: queueJobs.map(job => job.status), + injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length + }; + const originalStartUpload = startUpload; + let startCalls = 0; + startUpload = () => { startCalls++; return Promise.resolve(); }; + const prepareManualSelection = (filePath, autoStartValue, isUploading) => { + const file = { path: filePath, name: filePath.split('\\\\').pop(), size: 1 }; + resetQueue(autoStartValue); + uploading = isUploading; + _pendingFiles = [file]; + _pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [file] }; + _pendingImportInspections = 0; + const input = document.createElement('input'); + input.type = 'checkbox'; + input.dataset.hosterModal = 'doodstream.com'; + input.checked = true; + document.getElementById('hosterModalList').replaceChildren(input); + markPendingFolderMonitorFiles([file], autoStartValue); + return file; + }; + const injectBeforeManualQueue = window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length; + prepareManualSelection('C:\\\\folder-monitor-manual-queue.mkv', false, true); + applyHosterSelection(); + await new Promise(resolve => setTimeout(resolve, 0)); + const manualQueueOnly = { + statuses: queueJobs.map(job => job.status), + injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length - injectBeforeManualQueue + }; + const injectBeforeManualAuto = window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length; + prepareManualSelection('C:\\\\folder-monitor-manual-inject.mkv', true, true); + applyHosterSelection(); + await new Promise(resolve => setTimeout(resolve, 0)); + const manualAutoStartRunning = { + statuses: queueJobs.map(job => job.status), + injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length - injectBeforeManualAuto + }; + prepareManualSelection('C:\\\\folder-monitor-manual-start.mkv', true, false); + applyHosterSelection(); + await new Promise(resolve => setTimeout(resolve, 0)); + const manualAutoStartIdle = { startCalls }; + startUpload = originalStartUpload; + queueJobs = [ + { id: 'active-same-name', file: 'C:\\\\active\\\\same-name.mkv', fileName: 'same-name.mkv', hoster: 'doodstream.com', status: 'queued', bytesTotal: 1 }, + { id: 'waiting-same-name', file: 'D:\\\\watched\\\\same-name.mkv', fileName: 'same-name.mkv', hoster: 'doodstream.com', status: 'preview', bytesTotal: 1 } + ]; + rebuildJobIndex(); + applySummaryResults({ files: [{ name: 'same-name.mkv', size: 1, results: [{ hoster: 'doodstream.com', status: 'done', file_code: 'active-code' }] }] }); + const sameBasenameResult = Object.fromEntries(queueJobs.map(job => [job.id, { status: job.status, code: job.result?.file_code || null }])); + return { germanTooltips, englishTooltips, gridScope, actions, queueOnly, autoStart, manualQueueOnly, manualAutoStartRunning, manualAutoStartIdle, sameBasenameResult }; + })()`; const onlineBackupBehaviorScript = `(async () => { const ids = { a: 'AAAAAAAAAAAAAAAAAAAAAA', @@ -466,6 +576,7 @@ app.whenReady().then(async () => { const appDialogBehavior = await window.webContents.executeJavaScript(${JSON.stringify(appDialogBehaviorScript)}); const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)}); const settingsSearchBehavior = await window.webContents.executeJavaScript(${JSON.stringify(settingsSearchBehaviorScript)}); + const folderMonitorBehavior = await window.webContents.executeJavaScript(${JSON.stringify(folderMonitorBehaviorScript)}); const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)}); window.setContentSize(760, Math.min(900, display.workAreaSize.height)); await new Promise(resolve => setTimeout(resolve, 50)); @@ -483,6 +594,7 @@ app.whenReady().then(async () => { liveSpeedChart, appDialogBehavior, settingsSearchBehavior, + folderMonitorBehavior, onlineBackupBehavior, onlineBackupLayout, onlineBackupNarrowLayout @@ -587,6 +699,41 @@ app.whenReady().then(async () => { emptyHidden: true } }); + assert.deepEqual(result.folderMonitorBehavior, { + germanTooltips: { + enabled: { tooltip: 'Startet die Überwachung nach dem Speichern, wenn ein Ordner ausgewählt ist.', label: 'Startet die Überwachung nach dem Speichern, wenn ein Ordner ausgewählt ist.' }, + recursive: { tooltip: 'Überwacht zusätzlich alle Unterordner des ausgewählten Ordners.', label: 'Überwacht zusätzlich alle Unterordner des ausgewählten Ordners.' }, + existing: { tooltip: 'Fügt beim nächsten Start der Überwachung alle bereits vorhandenen passenden Dateien hinzu. Die Option wird danach automatisch deaktiviert.', label: 'Fügt beim nächsten Start der Überwachung alle bereits vorhandenen passenden Dateien hinzu. Die Option wird danach automatisch deaktiviert.' }, + duplicates: { tooltip: 'Ignoriert wiederholte Erkennungen desselben Dateipfads während der aktuellen Überwachung.', label: 'Ignoriert wiederholte Erkennungen desselben Dateipfads während der aktuellen Überwachung.' }, + 'auto-start': { tooltip: 'Startet neu erkannte Dateien automatisch. Ohne diese Option werden sie nur zur Warteschlange hinzugefügt.', label: 'Startet neu erkannte Dateien automatisch. Ohne diese Option werden sie nur zur Warteschlange hinzugefügt.' }, + hosters: { tooltip: 'Legt die Upload-Ziele für Dateien aus der Ordnerüberwachung fest. Ohne Auswahl ist eine manuelle Bestätigung erforderlich.', label: 'Legt die Upload-Ziele für Dateien aus der Ordnerüberwachung fest. Ohne Auswahl ist eine manuelle Bestätigung erforderlich.' } + }, + englishTooltips: { + enabled: { tooltip: 'Starts monitoring after saving when a folder is selected.', label: 'Starts monitoring after saving when a folder is selected.' }, + recursive: { tooltip: 'Also monitors every subfolder inside the selected folder.', label: 'Also monitors every subfolder inside the selected folder.' }, + existing: { tooltip: 'Adds all matching files already present when monitoring starts next. The option is then disabled automatically.', label: 'Adds all matching files already present when monitoring starts next. The option is then disabled automatically.' }, + duplicates: { tooltip: 'Ignores repeated detections of the same file path during the current monitoring session.', label: 'Ignores repeated detections of the same file path during the current monitoring session.' }, + 'auto-start': { tooltip: 'Starts newly detected files automatically. Without this option, they are only added to the queue.', label: 'Starts newly detected files automatically. Without this option, they are only added to the queue.' }, + hosters: { tooltip: 'Sets the upload destinations for files from folder monitoring. Without a selection, manual confirmation is required.', label: 'Sets the upload destinations for files from folder monitoring. Without a selection, manual confirmation is required.' } + }, + gridScope: { ordinary: false, folderMonitor: true }, + actions: { + disabledWhileIdle: 'queue', + disabledWhileUploading: 'queue', + enabledWhileIdle: 'start', + enabledWhileUploading: 'inject', + enabledDuringHealthCheck: 'queue' + }, + queueOnly: { statuses: ['preview'], injectCalls: 0 }, + autoStart: { statuses: ['queued'], injectCalls: 1 }, + manualQueueOnly: { statuses: ['preview'], injectCalls: 0 }, + manualAutoStartRunning: { statuses: ['queued'], injectCalls: 1 }, + manualAutoStartIdle: { startCalls: 1 }, + sameBasenameResult: { + 'active-same-name': { status: 'done', code: 'active-code' }, + 'waiting-same-name': { status: 'preview', code: null } + } + }); assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']); assert.deepEqual(result.onlineBackupBehavior.initialWarning, { hidden: false,