Compare commits

..

No commits in common. "5d04ddced3416e57c8e61d4e7ec8dc0c472c1c39" and "1e3f2ca51af16879a6398225b2cdf558d4122ddf" have entirely different histories.

3 changed files with 242 additions and 245 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.72", "version": "3.3.71",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -2906,30 +2906,15 @@ function renderSettings() {
const globalSettings = config.globalSettings || {}; const globalSettings = config.globalSettings || {};
const configuredAccounts = getAvailableHosters(); const configuredAccounts = getAvailableHosters();
const fm = globalSettings.folderMonitor || {}; const generalPanel = document.createElement('div');
const remoteSettings = globalSettings.remote || {}; generalPanel.className = 'hoster-settings-panel';
generalPanel.innerHTML = `
const subtabBar = document.createElement('div'); <div class="hoster-panel-header" data-hoster="global">
subtabBar.className = 'settings-subtabs'; <span class="panel-arrow">&#9660;</span>
subtabBar.innerHTML = ` <span class="panel-title">Allgemein</span>
<button class="settings-subtab active" data-subtab="allgemein">Allgemein</button> <span class="panel-status active">System</span>
<button class="settings-subtab" data-subtab="automatik">Automatik</button> </div>
<button class="settings-subtab" data-subtab="logs">Logs & Diagnose</button> <div class="hoster-panel-body" data-panel="global" style="display:block">
<button class="settings-subtab" data-subtab="remote">Fernsteuerung</button>
<button class="settings-subtab" data-subtab="backup">Backup</button>
`;
container.appendChild(subtabBar);
const pages = {};
['allgemein', 'automatik', 'logs', 'remote', 'backup'].forEach((id) => {
const page = document.createElement('div');
page.className = id === 'allgemein' ? 'settings-subpage active' : 'settings-subpage';
page.dataset.subpage = id;
pages[id] = page;
container.appendChild(page);
});
pages.allgemein.innerHTML = `
<div class="settings-section-label">Uploads</div> <div class="settings-section-label">Uploads</div>
<div class="settings-row"> <div class="settings-row">
<label style="min-width:185px">Globale parallele Uploads</label> <label style="min-width:185px">Globale parallele Uploads</label>
@ -2969,10 +2954,29 @@ function renderSettings() {
<label>Manuell prüfen</label> <label>Manuell prüfen</label>
<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>
<div class="settings-hoster-pointer">Die <strong>Upload-Einstellungen pro Hoster</strong> (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im <strong>Accounts</strong>-Tab direkt bei den jeweiligen Hostern unter Upload-Einstellungen".</div> <div class="settings-section-label">Log</div>
`; <div class="settings-row">
<label>FileUploader Log</label>
pages.automatik.innerHTML = ` <input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden">
<button class="btn btn-xs btn-secondary" id="chooseLogFilePathBtn">Ordner wählen</button>
<button class="btn btn-xs btn-secondary" id="openLogFolderBtn" title="Log-Ordner im Explorer öffnen">Öffnen</button>
</div>
<div class="settings-row">
<label>Log-Datei-Modus</label>
<select class="hs-input settings-autosave" id="logModeInput">
<option value="single" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'single' ? 'selected' : ''}>Eine Datei</option>
<option value="daily" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'daily' ? 'selected' : ''}>Pro Tag</option>
<option value="session" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'session' ? 'selected' : ''}>Pro Session</option>
</select>
<span class="hint">Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session.</span>
</div>
<div class="settings-row">
<label>Verbose Logging</label>
<label class="checkbox-row" style="margin:0">
<input type="checkbox" class="settings-autosave" id="logVerboseInput" ${globalSettings.logVerbose ? 'checked' : ''}>
<span>DEBUG-Einträge in debug.log schreiben (Performance , Diagnostik )</span>
</label>
</div>
<div class="settings-section-label">Unbeaufsichtigter Betrieb</div> <div class="settings-section-label">Unbeaufsichtigter Betrieb</div>
<div class="settings-row"> <div class="settings-row">
<label>Auto-Retry Runden</label> <label>Auto-Retry Runden</label>
@ -2995,7 +2999,97 @@ function renderSettings() {
<input type="text" class="key-input settings-autosave" id="webhookMentionInput" value="${escapeAttr(globalSettings.webhookMention || '')}" placeholder="deine User-ID, role:ROLLEN-ID, @here oder @everyone"> <input type="text" class="key-input settings-autosave" id="webhookMentionInput" value="${escapeAttr(globalSettings.webhookMention || '')}" placeholder="deine User-ID, role:ROLLEN-ID, @here oder @everyone">
<span class="hint">Damit Discord dich wirklich benachrichtigt (Push). User-ID: in Discord Entwicklermodus an Rechtsklick auf deinen Namen 'User-ID kopieren'. Leer = nur posten, kein Ping.</span> <span class="hint">Damit Discord dich wirklich benachrichtigt (Push). User-ID: in Discord Entwicklermodus an Rechtsklick auf deinen Namen 'User-ID kopieren'. Leer = nur posten, kein Ping.</span>
</div> </div>
<div class="settings-section-label">Ordnerüberwachung <span class="panel-status${fm.enabled && fm.folderPath ? ' active' : ''}" id="folderMonitorStatusBadge">${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}</span></div> <div class="settings-section-label">Diagnose</div>
<div class="settings-row" id="logPathsBlock">
<label>Log-Dateien</label>
<div class="log-paths-list" id="logPathsList" style="flex:1;display:flex;flex-direction:column;gap:4px">
<span class="hint">Wird geladen</span>
</div>
</div>
<div class="settings-row">
<label>Support-Paket</label>
<button class="btn btn-xs btn-secondary" id="createSupportBundleBtn" title="Sammelt alle Logs + sanitierte Config (Credentials maskiert) + App-Versionen in eine einzelne .txt-Datei zum Teilen.">Diagnose-Paket exportieren</button>
<span class="hint" id="supportBundleHint">Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert.</span>
</div>
</div>
`;
container.appendChild(generalPanel);
_renderLogPathsList(generalPanel.querySelector('#logPathsList'));
const testWebhookBtn = generalPanel.querySelector('#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 = generalPanel.querySelector('#logVerboseInput');
if (verboseInput) {
verboseInput.addEventListener('change', () => {
if (window.api && window.api.setLogVerbose) window.api.setLogVerbose(verboseInput.checked).catch(() => {});
});
}
const sbBtn = generalPanel.querySelector('#createSupportBundleBtn');
if (sbBtn) {
sbBtn.addEventListener('click', async () => {
const hint = generalPanel.querySelector('#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;
}
});
}
// Toggle general panel
generalPanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
const body = generalPanel.querySelector('.hoster-panel-body');
const arrow = generalPanel.querySelector('.panel-arrow');
const isOpen = body.style.display !== 'none';
body.style.display = isOpen ? 'none' : 'block';
arrow.innerHTML = isOpen ? '&#9654;' : '&#9660;';
});
// --- Folder Monitor Panel ---
const fm = globalSettings.folderMonitor || {};
const folderMonitorPanel = document.createElement('div');
folderMonitorPanel.className = 'hoster-settings-panel';
folderMonitorPanel.innerHTML = `
<div class="hoster-panel-header" data-hoster="folderMonitor">
<span class="panel-arrow">&#9654;</span>
<span class="panel-title">Ordnerüberwachung</span>
<span class="panel-status${fm.enabled && fm.folderPath ? ' active' : ''}" id="folderMonitorStatusBadge">${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}</span>
</div>
<div class="hoster-panel-body" data-panel="folderMonitor" style="display:none">
<div class="settings-section-label">Ordner</div>
<div class="settings-row"> <div class="settings-row">
<label>Ordnerpfad</label> <label>Ordnerpfad</label>
<input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen..." style="flex:1"> <input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen..." style="flex:1">
@ -3042,48 +3136,52 @@ function renderSettings() {
</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 Auswahl = Hoster-Modal bei jeder Datei.</p>'}
`;
pages.logs.innerHTML = `
<div class="settings-section-label">Log</div>
<div class="settings-row">
<label>FileUploader Log</label>
<input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden">
<button class="btn btn-xs btn-secondary" id="chooseLogFilePathBtn">Ordner wählen</button>
<button class="btn btn-xs btn-secondary" id="openLogFolderBtn" title="Log-Ordner im Explorer öffnen">Öffnen</button>
</div>
<div class="settings-row">
<label>Log-Datei-Modus</label>
<select class="hs-input settings-autosave" id="logModeInput">
<option value="single" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'single' ? 'selected' : ''}>Eine Datei</option>
<option value="daily" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'daily' ? 'selected' : ''}>Pro Tag</option>
<option value="session" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'session' ? 'selected' : ''}>Pro Session</option>
</select>
<span class="hint">Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session.</span>
</div>
<div class="settings-row">
<label>Verbose Logging</label>
<label class="checkbox-row" style="margin:0">
<input type="checkbox" class="settings-autosave" id="logVerboseInput" ${globalSettings.logVerbose ? 'checked' : ''}>
<span>DEBUG-Einträge in debug.log schreiben (Performance , Diagnostik )</span>
</label>
</div>
<div class="settings-section-label">Diagnose</div>
<div class="settings-row" id="logPathsBlock">
<label>Log-Dateien</label>
<div class="log-paths-list" id="logPathsList" style="flex:1;display:flex;flex-direction:column;gap:4px">
<span class="hint">Wird geladen</span>
</div>
</div>
<div class="settings-row">
<label>Support-Paket</label>
<button class="btn btn-xs btn-secondary" id="createSupportBundleBtn" title="Sammelt alle Logs + sanitierte Config (Credentials maskiert) + App-Versionen in eine einzelne .txt-Datei zum Teilen.">Diagnose-Paket exportieren</button>
<span class="hint" id="supportBundleHint">Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert.</span>
</div> </div>
`; `;
container.appendChild(folderMonitorPanel);
pages.remote.innerHTML = ` // Toggle folder monitor panel
<div class="settings-section-label">Server <span class="panel-status${remoteSettings.enabled ? ' active' : ''}" id="remoteStatusBadge">${remoteSettings.enabled ? 'Aktiv' : 'Inaktiv'}</span></div> folderMonitorPanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
const body = folderMonitorPanel.querySelector('.hoster-panel-body');
const arrow = folderMonitorPanel.querySelector('.panel-arrow');
const isOpen = body.style.display !== 'none';
body.style.display = isOpen ? 'none' : 'block';
arrow.innerHTML = isOpen ? '&#9654;' : '&#9660;';
});
// Update badge immediately on checkbox/path change
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();
}
});
// --- Remote Control Panel ---
const remoteSettings = globalSettings.remote || {};
const remotePanel = document.createElement('div');
remotePanel.className = 'hoster-settings-panel';
remotePanel.innerHTML = `
<div class="hoster-panel-header" data-hoster="remote">
<span class="panel-arrow">&#9654;</span>
<span class="panel-title">Fernsteuerung</span>
<span class="panel-status${remoteSettings.enabled ? ' active' : ''}" id="remoteStatusBadge">${remoteSettings.enabled ? 'Aktiv' : 'Inaktiv'}</span>
</div>
<div class="hoster-panel-body" data-panel="remote" style="display:none">
<div class="settings-section-label">Server</div>
<div class="settings-grid-mini"> <div class="settings-grid-mini">
<div class="settings-row checkbox-row"> <div class="settings-row checkbox-row">
<label>Aktiviert</label> <label>Aktiviert</label>
@ -3108,103 +3206,20 @@ function renderSettings() {
<div class="settings-row"> <div class="settings-row">
<span id="remoteConnectionStatus" style="color:#94a3b8">Prüfe...</span> <span id="remoteConnectionStatus" style="color:#94a3b8">Prüfe...</span>
</div> </div>
`;
pages.backup.innerHTML = `
<p class="hint" style="margin:0 0 10px">Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf bleibt lokal und wird nicht übertragen; nach einem Import ist der Verlauf-Tab leer.</p>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="exportBackupBtn">Backup exportieren</button>
<button class="btn btn-secondary" id="importBackupBtn">Backup importieren</button>
</div> </div>
`; `;
container.appendChild(remotePanel);
subtabBar.addEventListener('click', (e) => { // Toggle remote panel
const btn = e.target.closest('[data-subtab]'); remotePanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
if (!btn) return; const body = remotePanel.querySelector('.hoster-panel-body');
const target = btn.dataset.subtab; const arrow = remotePanel.querySelector('.panel-arrow');
subtabBar.querySelectorAll('.settings-subtab').forEach((b) => { const isOpen = body.style.display !== 'none';
b.classList.toggle('active', b === btn); body.style.display = isOpen ? 'none' : 'block';
}); arrow.innerHTML = isOpen ? '&#9654;' : '&#9660;';
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 () => { document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
const token = document.getElementById('remoteTokenInput').value; const token = document.getElementById('remoteTokenInput').value;
if (token) { if (token) {
@ -3214,12 +3229,14 @@ function renderSettings() {
} }
}); });
// Regenerate token
document.getElementById('remoteRegenerateTokenBtn').addEventListener('click', async () => { document.getElementById('remoteRegenerateTokenBtn').addEventListener('click', async () => {
const newToken = await window.api.remoteGenerateToken(); const newToken = await window.api.remoteGenerateToken();
document.getElementById('remoteTokenInput').value = newToken; document.getElementById('remoteTokenInput').value = newToken;
scheduleSettingsSave(); scheduleSettingsSave();
}); });
// Update status
window.api.remoteStatus().then(status => { window.api.remoteStatus().then(status => {
const el = document.getElementById('remoteConnectionStatus'); const el = document.getElementById('remoteConnectionStatus');
if (!el) return; if (!el) return;
@ -3232,9 +3249,43 @@ function renderSettings() {
} }
}).catch(() => {}); }).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 = `
<div class="hoster-panel-header" data-hoster="backup">
<span class="panel-arrow">&#9654;</span>
<span class="panel-title">Backup</span>
<span class="panel-status active">System</span>
</div>
<div class="hoster-panel-body" data-panel="backup" style="display:none">
<p class="hint" style="margin:0 0 10px">Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf bleibt lokal und wird nicht übertragen; nach einem Import ist der Verlauf-Tab leer.</p>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="exportBackupBtn">Backup exportieren</button>
<button class="btn btn-secondary" id="importBackupBtn">Backup importieren</button>
</div>
</div>
`;
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 ? '&#9654;' : '&#9660;';
});
document.getElementById('exportBackupBtn').addEventListener('click', () => doBackupExport()); document.getElementById('exportBackupBtn').addEventListener('click', () => doBackupExport());
document.getElementById('importBackupBtn').addEventListener('click', () => doBackupImport()); document.getElementById('importBackupBtn').addEventListener('click', () => doBackupImport());
const hosterPointer = document.createElement('div');
hosterPointer.className = 'settings-hoster-pointer';
hosterPointer.innerHTML = 'Die <strong>Upload-Einstellungen pro Hoster</strong> (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im <strong>Accounts</strong>-Tab — direkt bei den jeweiligen Hostern unter „Upload-Einstellungen".';
container.appendChild(hosterPointer);
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath); document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder()); document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => { document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => {
@ -3282,62 +3333,42 @@ function scheduleSettingsSave() {
async function saveSettings(options = {}) { async function saveSettings(options = {}) {
const { feedbackText = 'Gespeichert!' } = options; const { feedbackText = 'Gespeichert!' } = options;
const newHosterSettings = { ...(config.hosterSettings || {}) }; 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 = { const globalSettings = {
...cur, ...(config.globalSettings || {}),
logFilePath: elTxt('logFilePathInput', cur.logFilePath || '').trim(), logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
logMode: (() => { logMode: (() => {
const el = document.getElementById('logModeInput'); const v = document.getElementById('logModeInput')?.value;
if (!el) return cur.logMode || 'single';
const v = el.value;
return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single'; return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single';
})(), })(),
resumeQueueOnLaunch: elChk('resumeQueueOnLaunchInput', cur.resumeQueueOnLaunch !== false), resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
parallelUploadCount: elInt('parallelUploadCountInput', cur.parallelUploadCount ?? 0, 0, 0, 100), parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads), scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone), removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked,
showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget), showDropTarget: !!document.getElementById('showDropTargetInput')?.checked,
globalMaxSpeedKbs: (() => { globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024)),
const el = document.getElementById('globalMaxSpeedMbsInput'); logVerbose: !!document.getElementById('logVerboseInput')?.checked,
if (!el) return cur.globalMaxSpeedKbs ?? 0; webhookUrl: (document.getElementById('webhookUrlInput')?.value || '').trim(),
return Math.max(0, Math.round((parseFloat(el.value || '0') || 0) * 1024)); webhookMention: (document.getElementById('webhookMentionInput')?.value || '').trim(),
})(), autoRetryRounds: Math.max(0, Math.min(5, parseInt(document.getElementById('autoRetryRoundsInput')?.value || '0', 10) || 0)),
logVerbose: elChk('logVerboseInput', !!cur.logVerbose), autoRetryDelayMin: Math.max(1, Math.min(120, parseInt(document.getElementById('autoRetryDelayMinInput')?.value || '5', 10) || 5)),
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: { folderMonitor: {
...curFm, ...((config.globalSettings || {}).folderMonitor || {}),
enabled: elChk('fmEnabledInput', !!curFm.enabled), enabled: !!document.getElementById('fmEnabledInput')?.checked,
folderPath: elTxt('fmFolderPathInput', curFm.folderPath || '').trim(), folderPath: (document.getElementById('fmFolderPathInput')?.value || '').trim(),
recursive: elChk('fmRecursiveInput', !!curFm.recursive), recursive: !!document.getElementById('fmRecursiveInput')?.checked,
filterMode: (() => { const el = document.getElementById('fmFilterModeInput'); return el ? (el.value || 'include') : (curFm.filterMode || 'include'); })(), filterMode: document.getElementById('fmFilterModeInput')?.value || 'include',
extensions: elTxt('fmExtensionsInput', curFm.extensions || '').trim(), extensions: (document.getElementById('fmExtensionsInput')?.value || '').trim(),
skipDuplicates: elChk('fmSkipDuplicatesInput', curFm.skipDuplicates !== false), skipDuplicates: !!document.getElementById('fmSkipDuplicatesInput')?.checked,
delaySec: elInt('fmDelaySecInput', curFm.delaySec ?? 3, 3, 1, 300), delaySec: Math.max(1, parseInt(document.getElementById('fmDelaySecInput')?.value || '3', 10) || 3),
autoStart: elChk('fmAutoStartInput', curFm.autoStart !== false), autoStart: !!document.getElementById('fmAutoStartInput')?.checked,
hosters: document.querySelector('.fm-hoster-checkbox') hosters: Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
? Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
: (curFm.hosters || [])
}, },
remote: { remote: {
...curRemote, ...((config.globalSettings || {}).remote || {}),
enabled: elChk('remoteEnabledInput', !!curRemote.enabled), enabled: !!document.getElementById('remoteEnabledInput')?.checked,
port: elInt('remotePortInput', curRemote.port || 9100, 9100, 1024, 65535), port: Math.max(1024, Math.min(65535, parseInt(document.getElementById('remotePortInput')?.value || '9100', 10) || 9100)),
token: elTxt('remoteTokenInput', curRemote.token || '').trim(), token: (document.getElementById('remoteTokenInput')?.value || '').trim(),
allowInput: elChk('remoteAllowInputInput', curRemote.allowInput !== false) allowInput: !!document.getElementById('remoteAllowInputInput')?.checked
} }
}; };
@ -4740,9 +4771,6 @@ function setupListeners() {
// Virtual scroll for large queues // Virtual scroll for large queues
const queueContainer = document.getElementById('queueContainer'); const queueContainer = document.getElementById('queueContainer');
if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true }); if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true });
if (queueContainer && typeof window.ResizeObserver !== 'undefined') {
new window.ResizeObserver(_onQueueScroll).observe(queueContainer);
}
// Queue table sorting // Queue table sorting
document.querySelectorAll('#queueTable th.sortable').forEach(th => { document.querySelectorAll('#queueTable th.sortable').forEach(th => {

View File

@ -842,37 +842,6 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
.settings-divider { height: 1px; background: var(--border); margin: 12px 0; } .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; } .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 { .settings-row {
display: flex; display: flex;
align-items: center; align-items: center;