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.
This commit is contained in:
+96
-63
@@ -408,6 +408,66 @@ window.addEventListener('unhandledrejection', (e) => {
|
|||||||
} catch {}
|
} 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 ---
|
// --- Init ---
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
@@ -501,56 +561,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Folder monitor: auto-queue new files
|
window.api.onFolderMonitorNewFiles(handleFolderMonitorFiles);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Account switched notification
|
// Account switched notification
|
||||||
window.api.onAccountSwitched((data) => {
|
window.api.onAccountSwitched((data) => {
|
||||||
@@ -1163,6 +1174,10 @@ function applyHosterSelection() {
|
|||||||
const admittedFiles = _pendingFiles.filter(file => window.ImportPreflight
|
const admittedFiles = _pendingFiles.filter(file => window.ImportPreflight
|
||||||
.getEligibleImportHosters(file, selectedUploadHosters, hosterSettings).length > 0);
|
.getEligibleImportHosters(file, selectedUploadHosters, hosterSettings).length > 0);
|
||||||
const pendingPaths = new Set(admittedFiles.map(f => f.path));
|
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) {
|
if (admittedFiles.length > 0) {
|
||||||
selectedFiles.push(...admittedFiles);
|
selectedFiles.push(...admittedFiles);
|
||||||
}
|
}
|
||||||
@@ -1173,9 +1188,9 @@ function applyHosterSelection() {
|
|||||||
// During an active upload, build preview jobs for the new files and inject
|
// 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
|
// them into the running batch immediately (otherwise they'd be lost on
|
||||||
// handleBatchDone via syncSelectedFilesFromQueue)
|
// handleBatchDone via syncSelectedFilesFromQueue)
|
||||||
if (uploading && pendingPaths.size > 0) {
|
if (pendingPaths.size > 0) buildQueuePreview();
|
||||||
buildQueuePreview(); // creates 'preview' jobs for new files
|
if (uploading && pathsToInject.size > 0) {
|
||||||
const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file));
|
const newJobs = queueJobs.filter(j => j.status === 'preview' && pathsToInject.has(j.file));
|
||||||
if (newJobs.length > 0) {
|
if (newJobs.length > 0) {
|
||||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
||||||
newJobs.forEach(j => { j.status = 'queued'; });
|
newJobs.forEach(j => { j.status = 'queued'; });
|
||||||
@@ -1193,8 +1208,10 @@ function applyHosterSelection() {
|
|||||||
|
|
||||||
updateUploadView();
|
updateUploadView();
|
||||||
persistQueueStateSoon(true); // immediate persist after adding files
|
persistQueueStateSoon(true); // immediate persist after adding files
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
_pendingImportInspection = null;
|
_pendingImportInspection = null;
|
||||||
document.getElementById('hosterModal').style.display = 'none';
|
document.getElementById('hosterModal').style.display = 'none';
|
||||||
|
if (shouldAutoStart) startUpload();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1203,6 +1220,7 @@ function cancelHosterModal() {
|
|||||||
_pendingImportInspections = 0;
|
_pendingImportInspections = 0;
|
||||||
_importCoordination = Promise.resolve();
|
_importCoordination = Promise.resolve();
|
||||||
_pendingFiles = [];
|
_pendingFiles = [];
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
_pendingImportInspection = null;
|
_pendingImportInspection = null;
|
||||||
syncImportConfirmationState();
|
syncImportConfirmationState();
|
||||||
closeHosterModal();
|
closeHosterModal();
|
||||||
@@ -1420,6 +1438,7 @@ function setupDragDrop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _pendingFiles = [];
|
let _pendingFiles = [];
|
||||||
|
const _pendingFolderMonitorAutoStart = new Map();
|
||||||
let _pendingImportInspection = null;
|
let _pendingImportInspection = null;
|
||||||
let _importCoordination = Promise.resolve();
|
let _importCoordination = Promise.resolve();
|
||||||
let _importGeneration = 0;
|
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 : [];
|
const candidates = Array.isArray(entries) ? entries : [];
|
||||||
if (candidates.length === 0) return Promise.resolve(null);
|
if (candidates.length === 0) return Promise.resolve(null);
|
||||||
const generation = _importGeneration;
|
const generation = _importGeneration;
|
||||||
@@ -1535,6 +1558,9 @@ function coordinateImportEntries(entries) {
|
|||||||
if (generation !== _importGeneration) return null;
|
if (generation !== _importGeneration) return null;
|
||||||
mergePendingImportInspection(inspection);
|
mergePendingImportInspection(inspection);
|
||||||
if (inspection.accepted.length > 0) {
|
if (inspection.accepted.length > 0) {
|
||||||
|
if (typeof options.folderMonitorAutoStart === 'boolean') {
|
||||||
|
markPendingFolderMonitorFiles(inspection.accepted, options.folderMonitorAutoStart);
|
||||||
|
}
|
||||||
if (document.getElementById('hosterModal')?.style.display === 'flex') {
|
if (document.getElementById('hosterModal')?.style.display === 'flex') {
|
||||||
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
|
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
|
||||||
.map(input => input.dataset.hosterModal);
|
.map(input => input.dataset.hosterModal);
|
||||||
@@ -1596,8 +1622,8 @@ async function pickFolder() {
|
|||||||
return enqueueImportEntries(paths);
|
return enqueueImportEntries(paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPathsToQueue(paths) {
|
function addPathsToQueue(paths, options) {
|
||||||
return coordinateImportEntries(paths);
|
return coordinateImportEntries(paths, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUploadView() {
|
function updateUploadView() {
|
||||||
@@ -4235,6 +4261,7 @@ function applySummaryResults(summary) {
|
|||||||
// otherwise become O(n²).
|
// otherwise become O(n²).
|
||||||
const jobByKey = new Map();
|
const jobByKey = new Map();
|
||||||
for (const j of queueJobs) {
|
for (const j of queueJobs) {
|
||||||
|
if (j.status === 'preview') continue;
|
||||||
jobByKey.set(`${j.fileName}\u0001${j.hoster}`, j);
|
jobByKey.set(`${j.fileName}\u0001${j.hoster}`, j);
|
||||||
}
|
}
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -4976,6 +5003,7 @@ function renderSettings() {
|
|||||||
<button class="btn btn-xs btn-secondary" id="manualUpdateCheckBtn">Nach Updates suchen</button>
|
<button class="btn btn-xs btn-secondary" id="manualUpdateCheckBtn">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
const folderMonitorHelp = (key, text) => `<span class="settings-help" tabindex="0" role="note" aria-label="${escapeAttr(text)}" data-tooltip="${escapeAttr(text)}" data-folder-monitor-help="${escapeAttr(key)}"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"></circle><path d="M9.8 9a2.3 2.3 0 0 1 4.4 1c0 1.8-2.2 2-2.2 3.7"></path><path d="M12 17h.01"></path></svg></span>`;
|
||||||
|
|
||||||
pages.uploads.innerHTML = `
|
pages.uploads.innerHTML = `
|
||||||
${pageHeader('Upload-Verhalten', 'Globale Leistung, Warteschlange und Verhalten nach einem erfolgreichen Upload.')}
|
${pageHeader('Upload-Verhalten', 'Globale Leistung, Warteschlange und Verhalten nach einem erfolgreichen Upload.')}
|
||||||
@@ -5068,28 +5096,33 @@ function renderSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-section-label">Verhalten</div>
|
<div class="settings-section-label">Verhalten</div>
|
||||||
<div class="settings-grid-mini">
|
<div class="settings-grid-mini">
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row folder-monitor-help-row">
|
||||||
<label>Aktiviert</label>
|
<label>Aktiviert</label>
|
||||||
<input type="checkbox" class="settings-autosave" id="fmEnabledInput" ${fm.enabled ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave" id="fmEnabledInput" ${fm.enabled ? 'checked' : ''}>
|
||||||
|
${folderMonitorHelp('enabled', 'Startet die Überwachung nach dem Speichern, wenn ein Ordner ausgewählt ist.')}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row folder-monitor-help-row">
|
||||||
<label>Unterordner einbeziehen</label>
|
<label>Unterordner einbeziehen</label>
|
||||||
<input type="checkbox" class="settings-autosave" id="fmRecursiveInput" ${fm.recursive ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave" id="fmRecursiveInput" ${fm.recursive ? 'checked' : ''}>
|
||||||
|
${folderMonitorHelp('recursive', 'Überwacht zusätzlich alle Unterordner des ausgewählten Ordners.')}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row folder-monitor-help-row">
|
||||||
<label>Vorhandene Dateien einmalig einlesen</label>
|
<label>Vorhandene Dateien einmalig einlesen</label>
|
||||||
<input type="checkbox" class="settings-autosave" id="fmIncludeExistingInput" ${fm.includeExisting ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave" id="fmIncludeExistingInput" ${fm.includeExisting ? 'checked' : ''}>
|
||||||
|
${folderMonitorHelp('existing', 'Fügt beim nächsten Start der Überwachung alle bereits vorhandenen passenden Dateien hinzu. Die Option wird danach automatisch deaktiviert.')}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row folder-monitor-help-row">
|
||||||
<label>Duplikate überspringen</label>
|
<label>Duplikate überspringen</label>
|
||||||
<input type="checkbox" class="settings-autosave" id="fmSkipDuplicatesInput" ${fm.skipDuplicates !== false ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave" id="fmSkipDuplicatesInput" ${fm.skipDuplicates !== false ? 'checked' : ''}>
|
||||||
|
${folderMonitorHelp('duplicates', 'Ignoriert wiederholte Erkennungen desselben Dateipfads während der aktuellen Überwachung.')}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row folder-monitor-help-row">
|
||||||
<label>Auto-Upload starten</label>
|
<label>Auto-Upload starten</label>
|
||||||
<input type="checkbox" class="settings-autosave" id="fmAutoStartInput" ${fm.autoStart !== false ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave" id="fmAutoStartInput" ${fm.autoStart !== false ? 'checked' : ''}>
|
||||||
|
${folderMonitorHelp('auto-start', 'Startet neu erkannte Dateien automatisch. Ohne diese Option werden sie nur zur Warteschlange hinzugefügt.')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section-label">Hoster-Vorauswahl</div>
|
<div class="settings-section-label">Hoster-Vorauswahl ${folderMonitorHelp('hosters', 'Legt die Upload-Ziele für Dateien aus der Ordnerüberwachung fest. Ohne Auswahl ist eine manuelle Bestätigung erforderlich.')}</div>
|
||||||
<div class="settings-grid-mini">
|
<div class="settings-grid-mini">
|
||||||
${configuredAccounts.map(({ name }) => `
|
${configuredAccounts.map(({ name }) => `
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row">
|
||||||
@@ -5097,7 +5130,7 @@ function renderSettings() {
|
|||||||
<input type="checkbox" class="settings-autosave fm-hoster-checkbox" data-fm-hoster="${name}" ${(fm.hosters || []).includes(name) ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave fm-hoster-checkbox" data-fm-hoster="${name}" ${(fm.hosters || []).includes(name) ? 'checked' : ''}>
|
||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
${configuredAccounts.length === 0 ? '<p class="hint" style="margin:0">Erst Accounts anlegen, dann hier auswählen.</p>' : '<p class="hint" style="margin:2px 0 0">Keine Auswahl = Hoster-Modal bei jeder Datei.</p>'}
|
${configuredAccounts.length === 0 ? '<p class="hint" style="margin:0">Erst Accounts anlegen, dann hier auswählen.</p>' : '<p class="hint" style="margin:2px 0 0">Keine Vorauswahl = manuelle Hoster-Auswahl für neu erkannte Dateien.</p>'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
pages.benachrichtigungen.innerHTML = `
|
pages.benachrichtigungen.innerHTML = `
|
||||||
|
|||||||
+7
-1
@@ -265,8 +265,14 @@
|
|||||||
['Duplikate überspringen', 'Skip duplicates'],
|
['Duplikate überspringen', 'Skip duplicates'],
|
||||||
['Auto-Upload starten', 'Start uploads automatically'],
|
['Auto-Upload starten', 'Start uploads automatically'],
|
||||||
['Hoster-Vorauswahl', 'Host preselection'],
|
['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.'],
|
['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.'],
|
['Meldungen nach einem abgeschlossenen Upload-Batch versenden.', 'Send notifications after a completed upload batch.'],
|
||||||
['Webhook-Adresse', 'Webhook URL'],
|
['Webhook-Adresse', 'Webhook URL'],
|
||||||
['Test senden', 'Send test'],
|
['Test senden', 'Send test'],
|
||||||
|
|||||||
@@ -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 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 .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 {
|
.settings-hoster-pointer {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 11px 13px;
|
padding: 11px 13px;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ test('Windows compositor paints the full hidden surface with an RDP session envi
|
|||||||
fs.writeFileSync(preloadPath, `
|
fs.writeFileSync(preloadPath, `
|
||||||
const { contextBridge } = require('electron');
|
const { contextBridge } = require('electron');
|
||||||
const managedOnlineBackupProbeCalls = [];
|
const managedOnlineBackupProbeCalls = [];
|
||||||
|
const folderMonitorProbeCalls = [];
|
||||||
const managedOnlineBackupIds = {
|
const managedOnlineBackupIds = {
|
||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
||||||
@@ -142,7 +143,17 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
pendingManagedOnlineBackupDelete = null;
|
pendingManagedOnlineBackupDelete = null;
|
||||||
pending.resolve({ ok: true, removedId: pending.id, notFound: false });
|
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');
|
`, 'utf8');
|
||||||
const appDialogBehaviorScript = `(async () => {
|
const appDialogBehaviorScript = `(async () => {
|
||||||
@@ -261,6 +272,105 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
};
|
};
|
||||||
return { germanState, updatePaths, exceptionalPaths, stableSectionPath, umlautAliasPath, managedBackupNavigation, navigationState, englishPath, clearedState };
|
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 onlineBackupBehaviorScript = `(async () => {
|
||||||
const ids = {
|
const ids = {
|
||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
@@ -466,6 +576,7 @@ app.whenReady().then(async () => {
|
|||||||
const appDialogBehavior = await window.webContents.executeJavaScript(${JSON.stringify(appDialogBehaviorScript)});
|
const appDialogBehavior = await window.webContents.executeJavaScript(${JSON.stringify(appDialogBehaviorScript)});
|
||||||
const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)});
|
const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)});
|
||||||
const settingsSearchBehavior = await window.webContents.executeJavaScript(${JSON.stringify(settingsSearchBehaviorScript)});
|
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)});
|
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
||||||
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
@@ -483,6 +594,7 @@ app.whenReady().then(async () => {
|
|||||||
liveSpeedChart,
|
liveSpeedChart,
|
||||||
appDialogBehavior,
|
appDialogBehavior,
|
||||||
settingsSearchBehavior,
|
settingsSearchBehavior,
|
||||||
|
folderMonitorBehavior,
|
||||||
onlineBackupBehavior,
|
onlineBackupBehavior,
|
||||||
onlineBackupLayout,
|
onlineBackupLayout,
|
||||||
onlineBackupNarrowLayout
|
onlineBackupNarrowLayout
|
||||||
@@ -587,6 +699,41 @@ app.whenReady().then(async () => {
|
|||||||
emptyHidden: true
|
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.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user