Aktiviert
@@ -3206,20 +3108,103 @@ function renderSettings() {
Prüfe...
-
`;
- container.appendChild(remotePanel);
- // Toggle remote panel
- remotePanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
- const body = remotePanel.querySelector('.hoster-panel-body');
- const arrow = remotePanel.querySelector('.panel-arrow');
- const isOpen = body.style.display !== 'none';
- body.style.display = isOpen ? 'none' : 'block';
- arrow.innerHTML = isOpen ? '▶' : '▼';
+ pages.backup.innerHTML = `
+
Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf bleibt lokal und wird nicht übertragen; nach einem Import ist der Verlauf-Tab leer.
+
+ Backup exportieren
+ Backup importieren
+
+ `;
+
+ subtabBar.addEventListener('click', (e) => {
+ const btn = e.target.closest('[data-subtab]');
+ if (!btn) return;
+ const target = btn.dataset.subtab;
+ subtabBar.querySelectorAll('.settings-subtab').forEach((b) => {
+ b.classList.toggle('active', b === btn);
+ });
+ Object.values(pages).forEach((p) => {
+ p.classList.toggle('active', p.dataset.subpage === target);
+ });
+ });
+
+ _renderLogPathsList(document.getElementById('logPathsList'));
+ const testWebhookBtn = document.getElementById('testWebhookBtn');
+ if (testWebhookBtn) {
+ testWebhookBtn.addEventListener('click', async () => {
+ const url = (document.getElementById('webhookUrlInput')?.value || '').trim();
+ const mention = (document.getElementById('webhookMentionInput')?.value || '').trim();
+ const hint = document.getElementById('webhookHint');
+ if (!url) { if (hint) hint.textContent = 'Keine URL eingetragen.'; return; }
+ testWebhookBtn.disabled = true;
+ const prev = testWebhookBtn.textContent;
+ testWebhookBtn.textContent = 'Sende…';
+ try {
+ const res = await window.api.testWebhook({ url, mention });
+ if (hint) hint.textContent = res && res.ok
+ ? `Test erfolgreich gesendet (HTTP ${res.status}).`
+ : `Test fehlgeschlagen: ${(res && (res.error || 'HTTP ' + res.status)) || 'unbekannt'}`;
+ } catch (err) {
+ if (hint) hint.textContent = `Test fehlgeschlagen: ${err.message || err}`;
+ } finally {
+ testWebhookBtn.disabled = false;
+ testWebhookBtn.textContent = prev;
+ }
+ });
+ }
+ const verboseInput = document.getElementById('logVerboseInput');
+ if (verboseInput) {
+ verboseInput.addEventListener('change', () => {
+ if (window.api && window.api.setLogVerbose) window.api.setLogVerbose(verboseInput.checked).catch(() => {});
+ });
+ }
+ const sbBtn = document.getElementById('createSupportBundleBtn');
+ if (sbBtn) {
+ sbBtn.addEventListener('click', async () => {
+ const hint = document.getElementById('supportBundleHint');
+ sbBtn.disabled = true;
+ const prevText = sbBtn.textContent;
+ sbBtn.textContent = 'Exportiere…';
+ try {
+ const res = await window.api.createSupportBundle();
+ if (res && res.ok) {
+ if (hint) hint.textContent = `Gespeichert: ${res.path} (${(res.bytes/1024).toFixed(1)} KB)`;
+ } else if (res && res.canceled) {
+ if (hint) hint.textContent = 'Abgebrochen.';
+ } else {
+ if (hint) hint.textContent = `Fehler: ${(res && res.error) || 'unbekannt'}`;
+ }
+ } catch (err) {
+ if (hint) hint.textContent = `Fehler: ${err.message || err}`;
+ } finally {
+ sbBtn.disabled = false;
+ sbBtn.textContent = prevText;
+ }
+ });
+ }
+
+ const updateFmBadge = () => {
+ const b = document.getElementById('folderMonitorStatusBadge');
+ if (!b) return;
+ const enabled = document.getElementById('fmEnabledInput')?.checked;
+ const hasPath = (document.getElementById('fmFolderPathInput')?.value || '').trim();
+ if (enabled && hasPath) { b.textContent = 'Aktiv'; b.className = 'panel-status active'; }
+ else { b.textContent = 'Inaktiv'; b.className = 'panel-status'; }
+ };
+ document.getElementById('fmEnabledInput')?.addEventListener('change', updateFmBadge);
+ document.getElementById('fmFolderPathInput')?.addEventListener('input', updateFmBadge);
+
+ document.getElementById('fmChooseFolderBtn')?.addEventListener('click', async () => {
+ const folder = await window.api.folderMonitorSelectFolder();
+ if (folder) {
+ document.getElementById('fmFolderPathInput').value = folder;
+ updateFmBadge();
+ scheduleSettingsSave();
+ }
});
- // Copy token
document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
const token = document.getElementById('remoteTokenInput').value;
if (token) {
@@ -3229,14 +3214,12 @@ function renderSettings() {
}
});
- // Regenerate token
document.getElementById('remoteRegenerateTokenBtn').addEventListener('click', async () => {
const newToken = await window.api.remoteGenerateToken();
document.getElementById('remoteTokenInput').value = newToken;
scheduleSettingsSave();
});
- // Update status
window.api.remoteStatus().then(status => {
const el = document.getElementById('remoteConnectionStatus');
if (!el) return;
@@ -3249,43 +3232,9 @@ function renderSettings() {
}
}).catch(() => {});
- // Live client count updates (listener registered once in init, not here)
-
- // --- Backup Panel ---
- const backupPanel = document.createElement('div');
- backupPanel.className = 'hoster-settings-panel';
- backupPanel.innerHTML = `
-
-
-
Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf bleibt lokal und wird nicht übertragen; nach einem Import ist der Verlauf-Tab leer.
-
- Backup exportieren
- Backup importieren
-
-
- `;
- container.appendChild(backupPanel);
-
- backupPanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
- const body = backupPanel.querySelector('.hoster-panel-body');
- const arrow = backupPanel.querySelector('.panel-arrow');
- const isOpen = body.style.display !== 'none';
- body.style.display = isOpen ? 'none' : 'block';
- arrow.innerHTML = isOpen ? '▶' : '▼';
- });
-
document.getElementById('exportBackupBtn').addEventListener('click', () => doBackupExport());
document.getElementById('importBackupBtn').addEventListener('click', () => doBackupImport());
- const hosterPointer = document.createElement('div');
- hosterPointer.className = 'settings-hoster-pointer';
- hosterPointer.innerHTML = 'Die
Upload-Einstellungen pro Hoster (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im
Accounts -Tab — direkt bei den jeweiligen Hostern unter „Upload-Einstellungen".';
- container.appendChild(hosterPointer);
-
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => {
@@ -3333,42 +3282,62 @@ function scheduleSettingsSave() {
async function saveSettings(options = {}) {
const { feedbackText = 'Gespeichert!' } = options;
const newHosterSettings = { ...(config.hosterSettings || {}) };
+ const cur = config.globalSettings || {};
+ const curFm = cur.folderMonitor || {};
+ const curRemote = cur.remote || {};
+ const elTxt = (id, fb) => { const el = document.getElementById(id); return el ? el.value : fb; };
+ const elChk = (id, fb) => { const el = document.getElementById(id); return el ? !!el.checked : fb; };
+ const elInt = (id, curVal, dflt, lo, hi) => {
+ const el = document.getElementById(id);
+ if (!el) return curVal;
+ const n = parseInt(el.value || String(dflt), 10) || dflt;
+ return Math.max(lo, Math.min(hi, n));
+ };
+
const globalSettings = {
- ...(config.globalSettings || {}),
- logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
+ ...cur,
+ logFilePath: elTxt('logFilePathInput', cur.logFilePath || '').trim(),
logMode: (() => {
- const v = document.getElementById('logModeInput')?.value;
+ const el = document.getElementById('logModeInput');
+ if (!el) return cur.logMode || 'single';
+ const v = el.value;
return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single';
})(),
- resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
- parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
- scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
- removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked,
- showDropTarget: !!document.getElementById('showDropTargetInput')?.checked,
- globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024)),
- logVerbose: !!document.getElementById('logVerboseInput')?.checked,
- webhookUrl: (document.getElementById('webhookUrlInput')?.value || '').trim(),
- webhookMention: (document.getElementById('webhookMentionInput')?.value || '').trim(),
- autoRetryRounds: Math.max(0, Math.min(5, parseInt(document.getElementById('autoRetryRoundsInput')?.value || '0', 10) || 0)),
- autoRetryDelayMin: Math.max(1, Math.min(120, parseInt(document.getElementById('autoRetryDelayMinInput')?.value || '5', 10) || 5)),
+ resumeQueueOnLaunch: elChk('resumeQueueOnLaunchInput', cur.resumeQueueOnLaunch !== false),
+ parallelUploadCount: elInt('parallelUploadCountInput', cur.parallelUploadCount ?? 0, 0, 0, 100),
+ scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
+ removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
+ showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget),
+ globalMaxSpeedKbs: (() => {
+ const el = document.getElementById('globalMaxSpeedMbsInput');
+ if (!el) return cur.globalMaxSpeedKbs ?? 0;
+ return Math.max(0, Math.round((parseFloat(el.value || '0') || 0) * 1024));
+ })(),
+ logVerbose: elChk('logVerboseInput', !!cur.logVerbose),
+ webhookUrl: elTxt('webhookUrlInput', cur.webhookUrl || '').trim(),
+ webhookMention: elTxt('webhookMentionInput', cur.webhookMention || '').trim(),
+ autoRetryRounds: elInt('autoRetryRoundsInput', cur.autoRetryRounds ?? 0, 0, 0, 5),
+ autoRetryDelayMin: elInt('autoRetryDelayMinInput', cur.autoRetryDelayMin ?? 5, 5, 1, 120),
folderMonitor: {
- ...((config.globalSettings || {}).folderMonitor || {}),
- enabled: !!document.getElementById('fmEnabledInput')?.checked,
- folderPath: (document.getElementById('fmFolderPathInput')?.value || '').trim(),
- recursive: !!document.getElementById('fmRecursiveInput')?.checked,
- filterMode: document.getElementById('fmFilterModeInput')?.value || 'include',
- extensions: (document.getElementById('fmExtensionsInput')?.value || '').trim(),
- skipDuplicates: !!document.getElementById('fmSkipDuplicatesInput')?.checked,
- delaySec: Math.max(1, parseInt(document.getElementById('fmDelaySecInput')?.value || '3', 10) || 3),
- autoStart: !!document.getElementById('fmAutoStartInput')?.checked,
- hosters: Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
+ ...curFm,
+ enabled: elChk('fmEnabledInput', !!curFm.enabled),
+ folderPath: elTxt('fmFolderPathInput', curFm.folderPath || '').trim(),
+ recursive: elChk('fmRecursiveInput', !!curFm.recursive),
+ filterMode: (() => { const el = document.getElementById('fmFilterModeInput'); return el ? (el.value || 'include') : (curFm.filterMode || 'include'); })(),
+ extensions: elTxt('fmExtensionsInput', curFm.extensions || '').trim(),
+ skipDuplicates: elChk('fmSkipDuplicatesInput', curFm.skipDuplicates !== false),
+ delaySec: elInt('fmDelaySecInput', curFm.delaySec ?? 3, 3, 1, 300),
+ autoStart: elChk('fmAutoStartInput', curFm.autoStart !== false),
+ hosters: document.querySelector('.fm-hoster-checkbox')
+ ? Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
+ : (curFm.hosters || [])
},
remote: {
- ...((config.globalSettings || {}).remote || {}),
- enabled: !!document.getElementById('remoteEnabledInput')?.checked,
- port: Math.max(1024, Math.min(65535, parseInt(document.getElementById('remotePortInput')?.value || '9100', 10) || 9100)),
- token: (document.getElementById('remoteTokenInput')?.value || '').trim(),
- allowInput: !!document.getElementById('remoteAllowInputInput')?.checked
+ ...curRemote,
+ enabled: elChk('remoteEnabledInput', !!curRemote.enabled),
+ port: elInt('remotePortInput', curRemote.port || 9100, 9100, 1024, 65535),
+ token: elTxt('remoteTokenInput', curRemote.token || '').trim(),
+ allowInput: elChk('remoteAllowInputInput', curRemote.allowInput !== false)
}
};
@@ -4771,6 +4740,9 @@ function setupListeners() {
// Virtual scroll for large queues
const queueContainer = document.getElementById('queueContainer');
if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true });
+ if (queueContainer && typeof window.ResizeObserver !== 'undefined') {
+ new window.ResizeObserver(_onQueueScroll).observe(queueContainer);
+ }
// Queue table sorting
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
diff --git a/renderer/styles.css b/renderer/styles.css
index c07011f..95d86a4 100644
--- a/renderer/styles.css
+++ b/renderer/styles.css
@@ -842,6 +842,37 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
.settings-divider { height: 1px; background: var(--border); margin: 12px 0; }
.hoster-panel-body h4 { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; font-weight: 500; }
+.settings-subtabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 14px;
+ border-bottom: 1px solid var(--border);
+ flex-wrap: wrap;
+}
+.settings-subtab {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ padding: 8px 14px;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: color 0.15s, border-color 0.15s;
+}
+.settings-subtab:hover { color: var(--text); }
+.settings-subtab.active { color: var(--text); border-bottom-color: var(--accent); }
+.settings-subpage {
+ display: none;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 4px 14px 14px;
+}
+.settings-subpage.active { display: block; }
+
.settings-row {
display: flex;
align-items: center;