release: Multi-Hoster-Upload v2.0.3

This commit is contained in:
Sucukdeluxe
2026-08-09 14:54:44 +02:00
parent 6cdccf71f2
commit 1c4d096cb9
31 changed files with 2304 additions and 131 deletions
+176 -17
View File
@@ -104,6 +104,7 @@ const queuePersistThrottle = (window.ThrottleTimer && window.ThrottleTimer.makeT
})();
let _restoredSnapshotSavedAt = null;
let settingsSaveTimer = null;
const settingsSaveCoordinator = window.SerializedRunner.createSerializedRunner(performSaveSettings);
let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 };
const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx';
@@ -453,6 +454,8 @@ async function _handleMenuAction(action) {
case 'add-folder': document.getElementById('addFolderBtn')?.click(); break;
case 'backup-export': doBackupExport(); break;
case 'backup-import': doBackupImport(); break;
case 'online-backup-create': doOnlineBackupCreate(); break;
case 'online-backup-restore': openOnlineBackupRestore(); break;
case 'restart': if (confirm('Anwendung neu starten?')) window.api.restartApp(); break;
case 'quit': window.api.quitApp(); break;
case 'open-settings': document.querySelector('.tab[data-view="settings"]')?.click(); break;
@@ -1829,6 +1832,7 @@ function copySelectedRecentLinks() {
// --- Backup export / import ---
async function doBackupExport() {
try {
await flushPendingSettingsSaves();
const result = await window.api.exportBackup();
if (result && result.ok) showCopyToast('Backup exportiert');
} catch (err) {
@@ -1836,6 +1840,102 @@ async function doBackupExport() {
}
}
function applyImportedConfig(importedConfig, message) {
config = importedConfig;
hosterSettings = config.hosterSettings || {};
ensureAccountStatusEntries();
syncSelectedUploadHosters();
alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop);
window.api.setAlwaysOnTop(alwaysOnTopState);
renderSettings();
renderAccounts();
renderHosterSummary();
renderHosterModal();
loadHistory();
showCopyToast(message);
}
function setOnlineBackupStatus(message, state = '') {
const status = document.getElementById('onlineBackupStatus');
if (!status) return;
status.textContent = message;
status.dataset.state = state;
}
function setOnlineBackupBusy(busy) {
const createButton = document.getElementById('createOnlineBackupBtn');
const restoreButton = document.getElementById('restoreOnlineBackupBtn');
if (createButton) createButton.disabled = busy;
if (restoreButton) restoreButton.disabled = busy || !/^MHU2-[A-Za-z0-9_-]{70}$/.test(document.getElementById('onlineBackupKeyInput')?.value.trim() || '');
}
async function doOnlineBackupCreate() {
if (doOnlineBackupCreate.busy) return;
doOnlineBackupCreate.busy = true;
try {
await flushPendingSettingsSaves();
openOnlineBackupView();
} catch (error) {
openOnlineBackupView();
setOnlineBackupStatus(error.message || String(error), 'error');
doOnlineBackupCreate.busy = false;
return;
}
setOnlineBackupBusy(true);
setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy');
try {
const result = await window.api.createOnlineBackup();
if (!result || !result.ok) throw new Error(result?.error || 'Online-Sicherung konnte nicht erstellt werden');
const output = document.getElementById('onlineBackupKeyOutput');
const copyButton = document.getElementById('copyOnlineBackupKeyBtn');
if (output) output.value = result.key;
if (copyButton) copyButton.disabled = false;
setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success');
showCopyToast('Online-Schlüssel erstellt');
} catch (error) {
setOnlineBackupStatus(error.message || String(error), 'error');
} finally {
setOnlineBackupBusy(false);
doOnlineBackupCreate.busy = false;
}
}
function openOnlineBackupView(focusRestore = false) {
document.querySelector('.tab[data-view="settings"]')?.click();
document.querySelector('[data-subtab="backup"]')?.click();
if (focusRestore) document.getElementById('onlineBackupKeyInput')?.focus();
}
function openOnlineBackupRestore() {
openOnlineBackupView(true);
}
async function doOnlineBackupRestore() {
const input = document.getElementById('onlineBackupKeyInput');
const key = input?.value.trim() || '';
if (!/^MHU2-[A-Za-z0-9_-]{70}$/.test(key)) {
setOnlineBackupStatus('Der Schlüssel muss mit MHU2- beginnen und exakt 75 Zeichen lang sein.', 'error');
input?.focus();
return;
}
setOnlineBackupBusy(true);
setOnlineBackupStatus('Speichere aktuelle Einstellungen…', 'busy');
try {
await flushPendingSettingsSaves();
setOnlineBackupStatus('Lade und entschlüssele Einstellungen…', 'busy');
const result = await window.api.restoreOnlineBackup(key);
if (!result || !result.ok) throw new Error(result?.error || 'Online-Sicherung konnte nicht importiert werden');
applyImportedConfig(result.config, 'Online-Backup importiert');
const warnings = Array.isArray(result.warnings) ? result.warnings : [];
if (warnings.length) setOnlineBackupStatus(`Einstellungen übernommen. Bitte prüfen: ${warnings.join(', ')}.`, 'warning');
else setOnlineBackupStatus('Alle Accounts und Einstellungen wurden übernommen.', 'success');
} catch (error) {
setOnlineBackupStatus(error.message || String(error), 'error');
} finally {
setOnlineBackupBusy(false);
}
}
function askLegacyBackupPassword(hint) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
@@ -1906,6 +2006,7 @@ function askLegacyBackupPassword(hint) {
async function doBackupImport(legacyPassword) {
const pw = typeof legacyPassword === 'string' ? legacyPassword : undefined;
try {
await flushPendingSettingsSaves();
const result = await window.api.importBackup(pw);
if (!result || result.canceled) return;
if (result.needsPassword) {
@@ -1914,16 +2015,10 @@ async function doBackupImport(legacyPassword) {
return;
}
if (result.ok) {
config = result.config;
hosterSettings = config.hosterSettings || {};
alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop);
window.api.setAlwaysOnTop(alwaysOnTopState);
renderSettings();
renderAccounts();
renderHosterSummary();
renderHosterModal();
loadHistory();
showCopyToast('Backup importiert');
applyImportedConfig(result.config, 'Backup importiert');
if (Array.isArray(result.warnings) && result.warnings.length) {
alert(`Backup importiert. Bitte prüfen: ${result.warnings.join(', ')}.`);
}
} else if (result.error) {
alert('Import fehlgeschlagen: ' + result.error);
}
@@ -3339,10 +3434,33 @@ function renderSettings() {
`;
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>
<p class="hint" style="margin:0 0 10px">Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf wird nicht übertragen und bleibt auf diesem Gerät.</p>
<section class="online-backup-panel" aria-labelledby="onlineBackupHeading">
<div>
<h3 id="onlineBackupHeading">Verschlüsseltes Online-Backup</h3>
<p>Die Verschlüsselung findet ausschließlich auf diesem Gerät statt. Der Server speichert nur verschlüsselte Daten.</p>
</div>
<div class="online-backup-action">
<button class="btn btn-primary" id="createOnlineBackupBtn">Neuen Schlüssel erzeugen</button>
<span class="hint">Jeder Export erzeugt einen neuen Schlüssel. Ältere Schlüssel bleiben gültig.</span>
</div>
<div class="online-backup-key-row">
<label for="onlineBackupKeyOutput">Dein neuer Schlüssel</label>
<input type="text" class="key-input" id="onlineBackupKeyOutput" readonly spellcheck="false" autocomplete="off" placeholder="Nach dem Export erscheint hier der 75-stellige Schlüssel">
<button class="btn btn-secondary" id="copyOnlineBackupKeyBtn" disabled>Kopieren</button>
</div>
<div class="online-backup-key-row">
<label for="onlineBackupKeyInput">Vorhandenen Schlüssel importieren</label>
<input type="password" class="key-input" id="onlineBackupKeyInput" maxlength="75" pattern="MHU2-[A-Za-z0-9_-]{70}" spellcheck="false" autocomplete="off" placeholder="MHU2-…">
<button class="btn btn-secondary" id="restoreOnlineBackupBtn" disabled>Online importieren</button>
</div>
<p class="online-backup-warning">Behandle den Schlüssel wie ein Passwort. Wer ihn besitzt, kann die verschlüsselten Einstellungen entschlüsseln.</p>
<div class="online-backup-status" id="onlineBackupStatus" role="status" aria-live="polite"></div>
</section>
<div class="settings-section-label">Lokales Datei-Backup</div>
<div class="backup-file-actions">
<button class="btn btn-secondary" id="exportBackupBtn">Datei exportieren</button>
<button class="btn btn-secondary" id="importBackupBtn">Datei importieren</button>
</div>
`;
@@ -3574,6 +3692,20 @@ function renderSettings() {
document.getElementById('exportBackupBtn').addEventListener('click', () => doBackupExport());
document.getElementById('importBackupBtn').addEventListener('click', () => doBackupImport());
document.getElementById('createOnlineBackupBtn').addEventListener('click', () => doOnlineBackupCreate());
document.getElementById('copyOnlineBackupKeyBtn').addEventListener('click', async () => {
const key = document.getElementById('onlineBackupKeyOutput').value;
if (!key) return;
await window.api.copyToClipboard(key);
showCopyToast('Online-Schlüssel kopiert');
});
document.getElementById('onlineBackupKeyInput').addEventListener('input', (event) => {
const valid = /^MHU2-[A-Za-z0-9_-]{70}$/.test(event.target.value.trim());
document.getElementById('restoreOnlineBackupBtn').disabled = !valid;
if (event.target.value && !valid) setOnlineBackupStatus('Der Schlüssel muss exakt 75 Zeichen lang sein.', '');
else setOnlineBackupStatus('', '');
});
document.getElementById('restoreOnlineBackupBtn').addEventListener('click', () => doOnlineBackupRestore());
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
@@ -3613,13 +3745,18 @@ function scheduleSettingsSave() {
if (feedback) feedback.textContent = 'Speichert...';
clearTimeout(settingsSaveTimer);
settingsSaveTimer = setTimeout(() => {
settingsSaveTimer = null;
saveSettings({ feedbackText: 'Automatisch gespeichert' }).catch((err) => {
if (feedback) feedback.textContent = `Speichern fehlgeschlagen: ${err.message}`;
});
}, 350);
}
async function saveSettings(options = {}) {
function saveSettings(options = {}) {
return settingsSaveCoordinator.run(options);
}
async function performSaveSettings(options = {}) {
const { feedbackText = 'Gespeichert!' } = options;
const newHosterSettings = { ...(config.hosterSettings || {}) };
const cur = config.globalSettings || {};
@@ -3721,6 +3858,7 @@ async function saveSettings(options = {}) {
config.globalSettings = globalSettings;
hosterSettings = newHosterSettings;
clearTimeout(settingsSaveTimer);
settingsSaveTimer = null;
// Start/stop folder monitor based on settings
const fmSettings = globalSettings.folderMonitor;
@@ -3949,12 +4087,20 @@ function _hosterGroupOpenState(name, summary) {
const _hosterGroupOpenMemory = new Map();
let _hosterSettingsSaveTimer = null;
const hosterSettingsSaveCoordinator = window.SerializedRunner.createSerializedRunner(performHosterSettingsSave);
function scheduleHosterSettingsSave() {
clearTimeout(_hosterSettingsSaveTimer);
_hosterSettingsSaveTimer = setTimeout(() => { saveHosterSettingsFromDom().catch(() => {}); }, 350);
_hosterSettingsSaveTimer = setTimeout(() => {
_hosterSettingsSaveTimer = null;
saveHosterSettingsFromDom().catch(() => {});
}, 350);
}
async function saveHosterSettingsFromDom() {
function saveHosterSettingsFromDom() {
return hosterSettingsSaveCoordinator.run();
}
async function performHosterSettingsSave() {
const newHosterSettings = { ...(config.hosterSettings || {}) };
for (const name of HOSTERS) {
const inputs = document.querySelectorAll(`.account-hoster-settings-body .hs-input[data-hoster="${name}"]`);
@@ -3973,6 +4119,19 @@ async function saveHosterSettingsFromDom() {
hosterSettings = newHosterSettings;
}
async function flushPendingSettingsSaves() {
await Promise.all([settingsSaveCoordinator.flush(), hosterSettingsSaveCoordinator.flush()]);
const flushSettings = settingsSaveTimer !== null;
const flushHosters = _hosterSettingsSaveTimer !== null;
clearTimeout(settingsSaveTimer);
clearTimeout(_hosterSettingsSaveTimer);
settingsSaveTimer = null;
_hosterSettingsSaveTimer = null;
if (flushSettings) await saveSettings({ feedbackText: 'Automatisch gespeichert' });
if (flushHosters) await saveHosterSettingsFromDom();
await Promise.all([settingsSaveCoordinator.flush(), hosterSettingsSaveCoordinator.flush()]);
}
function _buildHosterSettingsHtml(name) {
const hs = (config.hosterSettings && config.hosterSettings[name]) || {};
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? String(+(hs.maxSpeedKbs / 1024).toFixed(2)) : '0';
+3
View File
@@ -19,6 +19,8 @@
<div class="menu-submenu-dropdown" style="display:none">
<button class="menu-dropdown-item" data-menu-action="backup-export"><span>Exportieren</span></button>
<button class="menu-dropdown-item" data-menu-action="backup-import"><span>Importieren</span></button>
<button class="menu-dropdown-item" data-menu-action="online-backup-create"><span>Online-Schlüssel erstellen</span></button>
<button class="menu-dropdown-item" data-menu-action="online-backup-restore"><span>Online-Schlüssel importieren</span></button>
</div>
</div>
<div class="menu-separator"></div>
@@ -421,6 +423,7 @@
<script src="../lib/throttled-cache.js"></script>
<script src="../lib/coalesced-set.js"></script>
<script src="../lib/throttle-timer.js"></script>
<script src="../lib/serialized-runner.js"></script>
<script src="account-submit.js"></script>
<script src="app.js"></script>
</body>
+79
View File
@@ -1386,3 +1386,82 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
animation-iteration-count: 1 !important;
}
}
.online-backup-panel {
display: grid;
gap: 16px;
padding: 18px;
margin-bottom: 18px;
border: 1px solid var(--border);
border-radius: 12px;
background: color-mix(in srgb, var(--bg-card) 88%, var(--accent) 12%);
}
.online-backup-panel h3 {
margin: 0 0 6px;
font-size: 15px;
}
.online-backup-panel p {
margin: 0;
color: var(--text-dim);
line-height: 1.5;
}
.online-backup-action,
.backup-file-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.online-backup-key-row {
display: grid;
grid-template-columns: minmax(170px, auto) minmax(240px, 1fr) auto;
gap: 10px;
align-items: center;
}
.online-backup-key-row label {
color: var(--text);
font-size: 12px;
font-weight: 600;
}
.online-backup-key-row .key-input {
width: 100%;
min-width: 0;
font-family: Consolas, monospace;
}
.online-backup-panel .online-backup-warning {
padding: 10px 12px;
border-radius: 8px;
background: rgba(245, 158, 11, 0.1);
color: #fbbf24;
}
.online-backup-status {
min-height: 18px;
color: var(--text-dim);
font-size: 12px;
}
.online-backup-status[data-state="success"] {
color: var(--success);
}
.online-backup-status[data-state="error"] {
color: var(--danger);
}
.online-backup-status[data-state="warning"] {
color: #fbbf24;
}
@media (max-width: 820px) {
.online-backup-key-row {
grid-template-columns: 1fr;
}
}