release: Multi-Hoster-Upload v2.0.3
This commit is contained in:
+176
-17
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user