Compare commits
No commits in common. "master" and "v3.3.47" have entirely different histories.
@ -277,12 +277,7 @@ class ConfigStore {
|
|||||||
if (fs.existsSync(this.filePath)) {
|
if (fs.existsSync(this.filePath)) {
|
||||||
const existing = fs.readFileSync(this.filePath, 'utf-8');
|
const existing = fs.readFileSync(this.filePath, 'utf-8');
|
||||||
if (existing && existing.trim().length > 2) {
|
if (existing && existing.trim().length > 2) {
|
||||||
let isValid = false;
|
fs.writeFileSync(backupPath, existing, 'utf-8');
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(existing);
|
|
||||||
isValid = parsed && typeof parsed === 'object' && (parsed.hosters || parsed.hosterSettings || parsed.globalSettings);
|
|
||||||
} catch {}
|
|
||||||
if (isValid) fs.writeFileSync(backupPath, existing, 'utf-8');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
@ -233,20 +233,7 @@ async function installUpdate(onProgress) {
|
|||||||
// Stage: done
|
// Stage: done
|
||||||
if (onProgress) onProgress({ stage: 'done', percent: 100 });
|
if (onProgress) onProgress({ stage: 'done', percent: 100 });
|
||||||
|
|
||||||
const _doQuit = () => setTimeout(() => app.quit(), 900);
|
setTimeout(() => app.quit(), 900);
|
||||||
const _getActive = () => {
|
|
||||||
try { return globalThis._mhuUploadManagerRef && globalThis._mhuUploadManagerRef.getActiveJobCount ? globalThis._mhuUploadManagerRef.getActiveJobCount() : 0; }
|
|
||||||
catch { return 0; }
|
|
||||||
};
|
|
||||||
if (_getActive() > 0) {
|
|
||||||
const POLL_MS = 3000;
|
|
||||||
const poller = setInterval(() => {
|
|
||||||
if (_getActive() === 0) { clearInterval(poller); _doQuit(); }
|
|
||||||
}, POLL_MS);
|
|
||||||
setTimeout(() => { try { clearInterval(poller); } catch {} _doQuit(); }, 30 * 60 * 1000);
|
|
||||||
} else {
|
|
||||||
_doQuit();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (onProgress) onProgress({ stage: 'error', error: err.message });
|
if (onProgress) onProgress({ stage: 'error', error: err.message });
|
||||||
|
|||||||
@ -67,10 +67,6 @@ class UploadManager extends EventEmitter {
|
|||||||
return this._accountOverrides.get(hoster) || null;
|
return this._accountOverrides.get(hoster) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveJobCount() {
|
|
||||||
return this.activeJobs.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFailedAccount(hoster, accountId) {
|
clearFailedAccount(hoster, accountId) {
|
||||||
return this._failedAccounts.delete(`${hoster}:${accountId}`);
|
return this._failedAccounts.delete(`${hoster}:${accountId}`);
|
||||||
}
|
}
|
||||||
@ -314,32 +310,18 @@ class UploadManager extends EventEmitter {
|
|||||||
this._batchResults = results;
|
this._batchResults = results;
|
||||||
this._additionalPromises = []; // Track jobs added mid-batch via addJobs()
|
this._additionalPromises = []; // Track jobs added mid-batch via addJobs()
|
||||||
|
|
||||||
const DEDUP_CHUNK = 200;
|
for (const task of tasks) {
|
||||||
for (let i = 0; i < tasks.length; i += DEDUP_CHUNK) {
|
const fileName = path.basename(task.file);
|
||||||
if (signal.aborted) break;
|
if (!results.has(task.file)) {
|
||||||
const end = Math.min(i + DEDUP_CHUNK, tasks.length);
|
let size = 0;
|
||||||
for (let j = i; j < end; j++) {
|
try { size = fs.statSync(task.file).size; } catch {}
|
||||||
const task = tasks[j];
|
results.set(task.file, { name: fileName, size, results: [] });
|
||||||
if (!results.has(task.file)) {
|
|
||||||
const fileName = path.basename(task.file);
|
|
||||||
let size = 0;
|
|
||||||
try { size = fs.statSync(task.file).size; } catch {}
|
|
||||||
results.set(task.file, { name: fileName, size, results: [] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (end < tasks.length) await new Promise(setImmediate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._startStatsTimer();
|
this._startStatsTimer();
|
||||||
|
|
||||||
const SPAWN_CHUNK = 100;
|
const promises = tasks.map((task) => this._runJob(task, results, signal));
|
||||||
const promises = [];
|
|
||||||
for (let i = 0; i < tasks.length; i += SPAWN_CHUNK) {
|
|
||||||
if (signal.aborted) break;
|
|
||||||
const end = Math.min(i + SPAWN_CHUNK, tasks.length);
|
|
||||||
for (let j = i; j < end; j++) promises.push(this._runJob(tasks[j], results, signal));
|
|
||||||
if (end < tasks.length) await new Promise(setImmediate);
|
|
||||||
}
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
// Wait for any jobs added mid-batch via addJobs()
|
// Wait for any jobs added mid-batch via addJobs()
|
||||||
while (this._additionalPromises.length > 0) {
|
while (this._additionalPromises.length > 0) {
|
||||||
@ -560,16 +542,14 @@ class UploadManager extends EventEmitter {
|
|||||||
speedAbort = new AbortController();
|
speedAbort = new AbortController();
|
||||||
uploadSignalBundle = this._combineManySignals([signal, speedAbort.signal]);
|
uploadSignalBundle = this._combineManySignals([signal, speedAbort.signal]);
|
||||||
speedMonitor = setInterval(() => {
|
speedMonitor = setInterval(() => {
|
||||||
try {
|
if (currentSpeedKbs > 0 && currentSpeedKbs < settings.restartBelowKbs) {
|
||||||
if (currentSpeedKbs > 0 && currentSpeedKbs < settings.restartBelowKbs) {
|
if (!lowSpeedSince) lowSpeedSince = Date.now();
|
||||||
if (!lowSpeedSince) lowSpeedSince = Date.now();
|
if (Date.now() - lowSpeedSince > 6000) {
|
||||||
if (Date.now() - lowSpeedSince > 6000) {
|
speedAbort.abort();
|
||||||
speedAbort.abort();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lowSpeedSince = 0;
|
|
||||||
}
|
}
|
||||||
} catch (e) { this._rotLog('speed-monitor-error', { jobId, error: e && e.message }); }
|
} else {
|
||||||
|
lowSpeedSince = 0;
|
||||||
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -583,42 +563,41 @@ class UploadManager extends EventEmitter {
|
|||||||
const PROGRESS_EMIT_INTERVAL = 250; // ms – throttle UI updates
|
const PROGRESS_EMIT_INTERVAL = 250; // ms – throttle UI updates
|
||||||
|
|
||||||
const progressCb = (bytesUploaded, bytesTotal) => {
|
const progressCb = (bytesUploaded, bytesTotal) => {
|
||||||
try {
|
const now = Date.now();
|
||||||
const now = Date.now();
|
const elapsed = Math.round((now - jobStart) / 1000);
|
||||||
const elapsed = Math.round((now - jobStart) / 1000);
|
const timeDelta = (now - lastSpeedTime) / 1000;
|
||||||
const timeDelta = (now - lastSpeedTime) / 1000;
|
if (timeDelta >= 1) {
|
||||||
if (Number.isFinite(timeDelta) && timeDelta >= 1) {
|
const bytesDelta = bytesUploaded - lastBytes;
|
||||||
const bytesDelta = bytesUploaded - lastBytes;
|
currentSpeedKbs = Math.round(bytesDelta / timeDelta / 1024);
|
||||||
currentSpeedKbs = Math.round(bytesDelta / timeDelta / 1024);
|
lastBytes = bytesUploaded;
|
||||||
lastBytes = bytesUploaded;
|
lastSpeedTime = now;
|
||||||
lastSpeedTime = now;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
activeEntry.speedKbs = currentSpeedKbs;
|
activeEntry.speedKbs = currentSpeedKbs;
|
||||||
activeEntry.bytesUploaded = bytesUploaded;
|
activeEntry.bytesUploaded = bytesUploaded;
|
||||||
|
|
||||||
if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return;
|
// Throttle progress emissions to reduce IPC + rendering overhead
|
||||||
lastEmitTime = now;
|
if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return;
|
||||||
|
lastEmitTime = now;
|
||||||
|
|
||||||
const remaining = currentSpeedKbs > 0
|
const remaining = currentSpeedKbs > 0
|
||||||
? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024))
|
? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
|
this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
|
||||||
jobId,
|
jobId,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
|
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
|
||||||
bytesUploaded,
|
bytesUploaded,
|
||||||
bytesTotal,
|
bytesTotal,
|
||||||
speedKbs: currentSpeedKbs,
|
speedKbs: currentSpeedKbs,
|
||||||
elapsed,
|
elapsed,
|
||||||
remaining,
|
remaining,
|
||||||
error: null,
|
error: null,
|
||||||
result: null,
|
result: null,
|
||||||
attempt,
|
attempt,
|
||||||
maxAttempts
|
maxAttempts
|
||||||
});
|
});
|
||||||
} catch { /* progress callbacks must never throw — swallowing is correct, the stream keeps going */ }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle);
|
const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle);
|
||||||
@ -1000,7 +979,7 @@ class UploadManager extends EventEmitter {
|
|||||||
_startStatsTimer() {
|
_startStatsTimer() {
|
||||||
if (this.statsInterval) clearInterval(this.statsInterval);
|
if (this.statsInterval) clearInterval(this.statsInterval);
|
||||||
this.statsInterval = setInterval(() => {
|
this.statsInterval = setInterval(() => {
|
||||||
try {
|
// Single pass over active jobs instead of two.
|
||||||
let globalSpeedKbs = 0;
|
let globalSpeedKbs = 0;
|
||||||
let activeCount = 0;
|
let activeCount = 0;
|
||||||
let inProgressBytes = 0;
|
let inProgressBytes = 0;
|
||||||
@ -1020,7 +999,6 @@ class UploadManager extends EventEmitter {
|
|||||||
activeJobs: activeCount,
|
activeJobs: activeCount,
|
||||||
pendingJobs: Object.values(this.semaphores).reduce((sum, semaphore) => sum + semaphore.pending, 0)
|
pendingJobs: Object.values(this.semaphores).reduce((sum, semaphore) => sum + semaphore.pending, 0)
|
||||||
});
|
});
|
||||||
} catch { /* never let a stats tick crash the timer + caller */ }
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -382,7 +382,7 @@ class VidmolyUploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (best && bestScore > 0) {
|
if (best && (bestScore > 0 || newFiles.length === 1)) {
|
||||||
return this._buildUrlsFromCode(best.file_code);
|
return this._buildUrlsFromCode(best.file_code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
301
main.js
301
main.js
@ -208,7 +208,6 @@ function getAllLogPaths() {
|
|||||||
debug: debugPath,
|
debug: debugPath,
|
||||||
accountRotation: rot,
|
accountRotation: rot,
|
||||||
doodstreamDebug: path.join(dir, 'doodstream-debug.log'),
|
doodstreamDebug: path.join(dir, 'doodstream-debug.log'),
|
||||||
crashLog: path.join(dir, 'crash.log'),
|
|
||||||
logDir: dir
|
logDir: dir
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -217,10 +216,23 @@ function rotLog(msg, ts) {
|
|||||||
try {
|
try {
|
||||||
const iso = new Date(ts || Date.now()).toISOString();
|
const iso = new Date(ts || Date.now()).toISOString();
|
||||||
const line = `[${iso}] ${msg}\n`;
|
const line = `[${iso}] ${msg}\n`;
|
||||||
_rotLogBuffer.push(line);
|
// Write synchronously. Rotation events are rare (a handful per batch) so
|
||||||
if (!_rotLogFlushTimer) {
|
// the batching optimization from debugLog doesn't buy us anything, and
|
||||||
_rotLogFlushTimer = setTimeout(() => { _rotLogFlushTimer = null; _flushRotLog(); }, 500);
|
// syncing guarantees the user can refresh the file and see fresh entries
|
||||||
|
// without waiting on a flush timer.
|
||||||
|
const candidates = [
|
||||||
|
getRotLogPath(),
|
||||||
|
path.join(app.getPath('desktop') || app.getPath('userData'), 'account-rotation.log'),
|
||||||
|
path.join(app.getPath('userData'), 'account-rotation.log')
|
||||||
|
];
|
||||||
|
for (const target of candidates) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
|
fs.appendFileSync(target, line, 'utf-8');
|
||||||
|
break;
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
// Mirror into the main debug log for single-file-grep convenience.
|
||||||
_debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`);
|
_debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`);
|
||||||
if (!_debugLogFlushTimer) {
|
if (!_debugLogFlushTimer) {
|
||||||
_debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500);
|
_debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500);
|
||||||
@ -228,62 +240,11 @@ function rotLog(msg, ts) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeSend(channel, data) {
|
// Catch unhandled rejections from fire-and-forget async calls
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return false;
|
|
||||||
try {
|
|
||||||
mainWindow.webContents.send(channel, data);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
debugLog(`safeSend(${channel}) failed: ${err && err.message ? err.message : err}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _writeCrashLog(prefix, err, extra) {
|
|
||||||
try {
|
|
||||||
const ts = new Date().toISOString();
|
|
||||||
const line = `[${ts}] ${prefix} ${err && err.stack ? err.stack : (err && err.message) || String(err)}${extra ? ' :: ' + JSON.stringify(extra) : ''}\n`;
|
|
||||||
try {
|
|
||||||
const target = getDebugLogPath();
|
|
||||||
fs.appendFileSync(target, line, 'utf-8');
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
const crashDir = path.dirname(getDebugLogPath());
|
|
||||||
fs.appendFileSync(path.join(crashDir, 'crash.log'), line, 'utf-8');
|
|
||||||
} catch {}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
|
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
|
||||||
_writeCrashLog('UNHANDLED REJECTION', reason);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', (err, origin) => {
|
|
||||||
_writeCrashLog('UNCAUGHT EXCEPTION (' + origin + ')', err);
|
|
||||||
debugLog(`UNCAUGHT EXCEPTION (${origin}): ${err && err.stack ? err.stack : err}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('exit', (code) => {
|
|
||||||
try { _writeCrashLog('PROCESS EXIT', new Error('code=' + code)); } catch {}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('warning', (warning) => {
|
|
||||||
debugLog(`PROCESS WARNING: ${warning.name} ${warning.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK']) {
|
|
||||||
try {
|
|
||||||
process.on(sig, () => {
|
|
||||||
_writeCrashLog('SIGNAL ' + sig, new Error('process received ' + sig));
|
|
||||||
try {
|
|
||||||
if (_debugLogBuffer.length) fs.appendFileSync(getDebugLogPath(), _debugLogBuffer.join(''), 'utf-8');
|
|
||||||
} catch {}
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function withTimeout(promise, timeoutMs, label) {
|
function withTimeout(promise, timeoutMs, label) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -483,7 +444,9 @@ function _flushUploadLog() {
|
|||||||
// next session writes here directly (no more fallback ladder) and
|
// next session writes here directly (no more fallback ladder) and
|
||||||
// the Settings input reflects reality.
|
// the Settings input reflects reality.
|
||||||
_persistFallbackLogPath(target.path);
|
_persistFallbackLogPath(target.path);
|
||||||
safeSend('upload-log-fallback', { fallbackPath: target.path });
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('upload-log-fallback', { fallbackPath: target.path });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_uploadLogBuffer.length && !_uploadLogFlushTimer) setImmediate(_flushUploadLog);
|
if (_uploadLogBuffer.length && !_uploadLogFlushTimer) setImmediate(_flushUploadLog);
|
||||||
});
|
});
|
||||||
@ -511,7 +474,9 @@ function _persistFallbackLogPath(workingPath) {
|
|||||||
cfg.globalSettings = gs;
|
cfg.globalSettings = gs;
|
||||||
configStore.save({ globalSettings: gs }).catch(() => {});
|
configStore.save({ globalSettings: gs }).catch(() => {});
|
||||||
_invalidateUploadLogTargetCache();
|
_invalidateUploadLogTargetCache();
|
||||||
safeSend('log-path-auto-updated', { logFilePath: toSave });
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('log-path-auto-updated', { logFilePath: toSave });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugLog(`persist fallback logpath failed: ${err.message}`);
|
debugLog(`persist fallback logpath failed: ${err.message}`);
|
||||||
}
|
}
|
||||||
@ -995,51 +960,6 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setBackgroundThrottling(false);
|
mainWindow.webContents.setBackgroundThrottling(false);
|
||||||
|
|
||||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
|
||||||
_writeCrashLog('RENDER PROCESS GONE', new Error(details.reason || 'unknown'), details);
|
|
||||||
debugLog(`RENDER PROCESS GONE: reason=${details.reason} exitCode=${details.exitCode}`);
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
try {
|
|
||||||
const choice = dialog.showMessageBoxSync(mainWindow, {
|
|
||||||
type: 'error',
|
|
||||||
title: 'Renderer abgestürzt',
|
|
||||||
message: `Der Renderer-Prozess ist abgestürzt (${details.reason}).`,
|
|
||||||
detail: 'Bitte Diagnose-Paket exportieren und einsenden. Klick "Neu laden" um die UI wiederherzustellen — laufende Uploads im Main-Process bleiben aktiv.',
|
|
||||||
buttons: ['Neu laden', 'Beenden'],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1
|
|
||||||
});
|
|
||||||
if (choice === 0) {
|
|
||||||
mainWindow.webContents.reload();
|
|
||||||
} else {
|
|
||||||
app.exit(1);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
try { mainWindow.webContents.reload(); } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.on('unresponsive', () => {
|
|
||||||
_writeCrashLog('RENDERER UNRESPONSIVE', new Error('webContents unresponsive'));
|
|
||||||
debugLog('RENDERER UNRESPONSIVE');
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.on('responsive', () => {
|
|
||||||
debugLog('RENDERER RESPONSIVE AGAIN');
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
|
||||||
_writeCrashLog('DID-FAIL-LOAD', new Error(errorDescription), { errorCode, validatedURL });
|
|
||||||
debugLog(`DID-FAIL-LOAD: ${errorCode} ${errorDescription} url=${validatedURL}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('child-process-gone', (_event, details) => {
|
|
||||||
_writeCrashLog('CHILD PROCESS GONE', new Error(details.reason || 'unknown'), details);
|
|
||||||
debugLog(`CHILD PROCESS GONE: type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1133,7 +1053,7 @@ app.whenReady().then(() => {
|
|||||||
logInfo(`update-check: available=${result && result.available}, remote=${result && result.remoteVersion}`);
|
logInfo(`update-check: available=${result && result.available}, remote=${result && result.remoteVersion}`);
|
||||||
logDebug(`update-check result: ${JSON.stringify(result)}`);
|
logDebug(`update-check result: ${JSON.stringify(result)}`);
|
||||||
if (result && result.available && mainWindow && !mainWindow.isDestroyed()) {
|
if (result && result.available && mainWindow && !mainWindow.isDestroyed()) {
|
||||||
safeSend('app:update-available', result);
|
mainWindow.webContents.send('app:update-available', result);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('update-check failed', err);
|
logError('update-check failed', err);
|
||||||
@ -1142,9 +1062,6 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
const activeJobs = uploadManager && typeof uploadManager.getActiveJobCount === 'function' ? uploadManager.getActiveJobCount() : 0;
|
|
||||||
debugLog(`window-all-closed: activeJobs=${activeJobs}, uploadManager=${!!uploadManager}`);
|
|
||||||
_writeCrashLog('WINDOW-ALL-CLOSED', new Error('all windows closed'), { activeJobs, uploadManager: !!uploadManager });
|
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1155,8 +1072,7 @@ app.on('before-quit', () => {
|
|||||||
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
|
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
|
||||||
destroyCaptureWindow();
|
destroyCaptureWindow();
|
||||||
} catch {}
|
} catch {}
|
||||||
try { destroyDropTargetWindow(); } catch {}
|
destroyDropTargetWindow();
|
||||||
try { if (tray && !tray.isDestroyed()) { tray.destroy(); tray = null; } } catch {}
|
|
||||||
// Flush pending log buffers synchronously so no lines are lost.
|
// Flush pending log buffers synchronously so no lines are lost.
|
||||||
try {
|
try {
|
||||||
if (_debugLogBuffer.length) {
|
if (_debugLogBuffer.length) {
|
||||||
@ -1217,9 +1133,11 @@ ipcMain.handle('save-config', async (_event, config) => {
|
|||||||
rotLog(`main: config-updated → late fallback ${fallback.id} for ${hoster} (was stuck on ${failedAccountId})`);
|
rotLog(`main: config-updated → late fallback ${fallback.id} for ${hoster} (was stuck on ${failedAccountId})`);
|
||||||
uploadManager.switchAccount(hoster, fallback);
|
uploadManager.switchAccount(hoster, fallback);
|
||||||
_sessionAccountOverrides.set(hoster, fallback);
|
_sessionAccountOverrides.set(hoster, fallback);
|
||||||
safeSend('account-switched', {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('account-switched', {
|
||||||
hoster, fromAccountId: failedAccountId, toAccountId: fallback.id
|
hoster, fromAccountId: failedAccountId, toAccountId: fallback.id
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1379,17 +1297,6 @@ ipcMain.handle('select-folder', async () => {
|
|||||||
});
|
});
|
||||||
if (result.canceled || !result.filePaths.length) return null;
|
if (result.canceled || !result.filePaths.length) return null;
|
||||||
|
|
||||||
const files = [];
|
|
||||||
for (const folder of result.filePaths) await walkFolderAsync(folder, files);
|
|
||||||
return files.length > 0 ? files.map(f => f.path) : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('select-folder-with-sizes', async () => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ['openDirectory', 'multiSelections']
|
|
||||||
});
|
|
||||||
if (result.canceled || !result.filePaths.length) return null;
|
|
||||||
|
|
||||||
const files = [];
|
const files = [];
|
||||||
for (const folder of result.filePaths) await walkFolderAsync(folder, files);
|
for (const folder of result.filePaths) await walkFolderAsync(folder, files);
|
||||||
return files.length > 0 ? files : null;
|
return files.length > 0 ? files : null;
|
||||||
@ -1407,11 +1314,7 @@ async function walkFolderAsync(rootDir, outFiles) {
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = path.join(dir, entry.name);
|
const full = path.join(dir, entry.name);
|
||||||
if (entry.isDirectory()) stack.push(full);
|
if (entry.isDirectory()) stack.push(full);
|
||||||
else if (entry.isFile()) {
|
else if (entry.isFile()) outFiles.push(full);
|
||||||
let size = 0;
|
|
||||||
try { size = (await fsp.stat(full)).size; } catch {}
|
|
||||||
outFiles.push({ path: full, name: entry.name, size });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ((++scanned % 8) === 0) await new Promise(setImmediate);
|
if ((++scanned % 8) === 0) await new Promise(setImmediate);
|
||||||
}
|
}
|
||||||
@ -1423,18 +1326,6 @@ ipcMain.handle('resolve-folder-files', async (_event, folderPath) => {
|
|||||||
return files;
|
return files;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-file-sizes', async (_event, paths) => {
|
|
||||||
if (!Array.isArray(paths)) return {};
|
|
||||||
const fsp = fs.promises;
|
|
||||||
const out = {};
|
|
||||||
let i = 0;
|
|
||||||
for (const p of paths) {
|
|
||||||
try { out[p] = (await fsp.stat(p)).size; } catch { out[p] = 0; }
|
|
||||||
if ((++i % 32) === 0) await new Promise(setImmediate);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('start-upload', (_event, payload) => {
|
ipcMain.handle('start-upload', (_event, payload) => {
|
||||||
const config = configStore.load();
|
const config = configStore.load();
|
||||||
const files = payload && Array.isArray(payload.files) ? payload.files : [];
|
const files = payload && Array.isArray(payload.files) ? payload.files : [];
|
||||||
@ -1490,7 +1381,6 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
|
|
||||||
// Pass hoster settings to the upload manager
|
// Pass hoster settings to the upload manager
|
||||||
uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {});
|
uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {});
|
||||||
globalThis._mhuUploadManagerRef = uploadManager;
|
|
||||||
|
|
||||||
const _progressByJob = new Map();
|
const _progressByJob = new Map();
|
||||||
const _progressTerminalQueue = [];
|
const _progressTerminalQueue = [];
|
||||||
@ -1508,7 +1398,7 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
const batch = _progressTerminalQueue.splice(0);
|
const batch = _progressTerminalQueue.splice(0);
|
||||||
for (const v of _progressByJob.values()) batch.push(v);
|
for (const v of _progressByJob.values()) batch.push(v);
|
||||||
_progressByJob.clear();
|
_progressByJob.clear();
|
||||||
if (batch.length) safeSend('upload-progress-batch', batch);
|
if (batch.length) mainWindow.webContents.send('upload-progress-batch', batch);
|
||||||
}, PROGRESS_BATCH_INTERVAL_MS);
|
}, PROGRESS_BATCH_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1546,16 +1436,16 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
uploadManager.on('stats', (data) => {
|
uploadManager.on('stats', (data) => {
|
||||||
try {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
if (!data || typeof data !== 'object') return;
|
mainWindow.webContents.send('upload-stats', data);
|
||||||
safeSend('upload-stats', data);
|
}
|
||||||
if (data.state === 'uploading' && data.activeJobs > 0) {
|
// Update tray tooltip with upload progress
|
||||||
const speedMb = ((Number(data.globalSpeedKbs) || 0) / 1024).toFixed(1);
|
if (data.state === 'uploading' && data.activeJobs > 0) {
|
||||||
updateTrayTooltip(`Upload: ${data.activeJobs} aktiv - ${speedMb} MB/s`);
|
const speedMb = ((data.globalSpeedKbs || 0) / 1024).toFixed(1);
|
||||||
} else {
|
updateTrayTooltip(`Upload: ${data.activeJobs} aktiv - ${speedMb} MB/s`);
|
||||||
updateTrayTooltip('Multi-Hoster-Upload');
|
} else {
|
||||||
}
|
updateTrayTooltip('Multi-Hoster-Upload');
|
||||||
} catch (e) { debugLog(`stats listener error: ${e && e.message}`); }
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadManager.on('account-failed', ({ hoster, accountId }) => {
|
uploadManager.on('account-failed', ({ hoster, accountId }) => {
|
||||||
@ -1568,7 +1458,9 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
rotLog(`main: account-failed ${hoster} ${accountId} → resolved fallback ${fallback.id}`);
|
rotLog(`main: account-failed ${hoster} ${accountId} → resolved fallback ${fallback.id}`);
|
||||||
uploadManager.switchAccount(hoster, fallback);
|
uploadManager.switchAccount(hoster, fallback);
|
||||||
_sessionAccountOverrides.set(hoster, fallback);
|
_sessionAccountOverrides.set(hoster, fallback);
|
||||||
safeSend('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rotLog(`main: account-failed ${hoster} ${accountId} → NO fallback available (end of chain)`);
|
rotLog(`main: account-failed ${hoster} ${accountId} → NO fallback available (end of chain)`);
|
||||||
}
|
}
|
||||||
@ -1584,25 +1476,17 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
'doodstream-via-web'
|
'doodstream-via-web'
|
||||||
]);
|
]);
|
||||||
uploadManager.on('rot-log', (entry) => {
|
uploadManager.on('rot-log', (entry) => {
|
||||||
try {
|
const { ts, event, ...rest } = entry;
|
||||||
if (!entry || typeof entry !== 'object') return;
|
const pairs = Object.entries(rest)
|
||||||
const { ts, event, ...rest } = entry;
|
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
||||||
const pairs = Object.entries(rest)
|
.join(' ');
|
||||||
.map(([k, v]) => {
|
rotLog(`[${event}] ${pairs}`, ts);
|
||||||
let sv;
|
if (entry.jobId) {
|
||||||
try { sv = typeof v === 'string' ? v : JSON.stringify(v); }
|
_appendJobLog(entry.jobId, { ts: ts || Date.now(), kind: 'rot', event, ...rest });
|
||||||
catch { sv = '<unserializable>'; }
|
}
|
||||||
return `${k}=${sv}`;
|
if (mainWindow && !mainWindow.isDestroyed() && ROT_LOG_RENDERER_EVENTS.has(event)) {
|
||||||
})
|
mainWindow.webContents.send('account-rotation-log', entry);
|
||||||
.join(' ');
|
}
|
||||||
rotLog(`[${event}] ${pairs}`, ts);
|
|
||||||
if (entry.jobId) {
|
|
||||||
_appendJobLog(entry.jobId, { ts: ts || Date.now(), kind: 'rot', event, ...rest });
|
|
||||||
}
|
|
||||||
if (ROT_LOG_RENDERER_EVENTS.has(event)) {
|
|
||||||
safeSend('account-rotation-log', entry);
|
|
||||||
}
|
|
||||||
} catch (e) { debugLog(`rot-log listener error: ${e && e.message}`); }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture the manager identity at listener-registration time so the post-
|
// Capture the manager identity at listener-registration time so the post-
|
||||||
@ -1619,11 +1503,13 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
try { await configStore.appendHistory(summary); } catch (err) {
|
try { await configStore.appendHistory(summary); } catch (err) {
|
||||||
debugLog(`appendHistory failed: ${err.message}`);
|
debugLog(`appendHistory failed: ${err.message}`);
|
||||||
}
|
}
|
||||||
safeSend('upload-batch-done', summary);
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('upload-batch-done', summary);
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown after finish
|
// Shutdown after finish
|
||||||
handleShutdownAfterFinish();
|
handleShutdownAfterFinish();
|
||||||
if (uploadManager === _thisManager) { uploadManager = null; globalThis._mhuUploadManagerRef = null; }
|
if (uploadManager === _thisManager) uploadManager = null;
|
||||||
else debugLog('batch-done: skipping uploadManager null-out — a newer manager replaced this one mid-await');
|
else debugLog('batch-done: skipping uploadManager null-out — a newer manager replaced this one mid-await');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1639,7 +1525,8 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
||||||
// Forward error to renderer as batch-done with failure
|
// Forward error to renderer as batch-done with failure
|
||||||
safeSend('upload-batch-done', {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('upload-batch-done', {
|
||||||
id: 'error',
|
id: 'error',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
total: tasks.length,
|
total: tasks.length,
|
||||||
@ -1648,6 +1535,7 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
files: [],
|
files: [],
|
||||||
error: err ? err.message : 'Unbekannter Fehler'
|
error: err ? err.message : 'Unbekannter Fehler'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1834,7 +1722,6 @@ ipcMain.handle('create-support-bundle', async () => {
|
|||||||
{ label: 'debug.log (last 5 MB)', path: paths.debug, maxBytes: 5 * 1024 * 1024 },
|
{ label: 'debug.log (last 5 MB)', path: paths.debug, maxBytes: 5 * 1024 * 1024 },
|
||||||
{ label: 'account-rotation.log (last 2 MB)', path: paths.accountRotation, maxBytes: 2 * 1024 * 1024 },
|
{ label: 'account-rotation.log (last 2 MB)', path: paths.accountRotation, maxBytes: 2 * 1024 * 1024 },
|
||||||
{ label: 'doodstream-debug.log (last 2 MB)', path: paths.doodstreamDebug, maxBytes: 2 * 1024 * 1024 },
|
{ label: 'doodstream-debug.log (last 2 MB)', path: paths.doodstreamDebug, maxBytes: 2 * 1024 * 1024 },
|
||||||
{ label: 'crash.log', path: path.join(paths.logDir || path.dirname(paths.debug), 'crash.log'), maxBytes: 1 * 1024 * 1024 },
|
|
||||||
{ label: 'fileuploader.log (last 1 MB)', path: paths.fileuploader, maxBytes: 1 * 1024 * 1024 }
|
{ label: 'fileuploader.log (last 1 MB)', path: paths.fileuploader, maxBytes: 1 * 1024 * 1024 }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -1876,19 +1763,12 @@ ipcMain.handle('export-backup', async () => {
|
|||||||
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
|
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
|
||||||
title: 'Backup exportieren',
|
title: 'Backup exportieren',
|
||||||
defaultPath: `multi-hoster-backup-${new Date().toISOString().slice(0, 10)}.mhu`,
|
defaultPath: `multi-hoster-backup-${new Date().toISOString().slice(0, 10)}.mhu`,
|
||||||
filters: [
|
filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }]
|
||||||
{ name: 'Multi-Hoster Backup (verschlüsselt)', extensions: ['mhu'] },
|
|
||||||
{ name: 'Multi-Hoster Backup (Klartext JSON)', extensions: ['json'] }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
if (canceled || !filePath) return { ok: false, canceled: true };
|
if (canceled || !filePath) return { ok: false, canceled: true };
|
||||||
const config = configStore.load();
|
const config = configStore.load();
|
||||||
if (filePath.toLowerCase().endsWith('.json')) {
|
const encrypted = backupCrypto.encrypt(config);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
|
fs.writeFileSync(filePath, encrypted);
|
||||||
} else {
|
|
||||||
const encrypted = backupCrypto.encrypt(config);
|
|
||||||
fs.writeFileSync(filePath, encrypted);
|
|
||||||
}
|
|
||||||
return { ok: true, path: filePath };
|
return { ok: true, path: filePath };
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1900,11 +1780,7 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
|
|||||||
} else {
|
} else {
|
||||||
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
|
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
|
||||||
title: 'Backup importieren',
|
title: 'Backup importieren',
|
||||||
filters: [
|
filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }],
|
||||||
{ name: 'Multi-Hoster Backup', extensions: ['mhu', 'json'] },
|
|
||||||
{ name: 'Verschlüsselt (.mhu)', extensions: ['mhu'] },
|
|
||||||
{ name: 'Klartext (.json)', extensions: ['json'] }
|
|
||||||
],
|
|
||||||
properties: ['openFile']
|
properties: ['openFile']
|
||||||
});
|
});
|
||||||
if (canceled || !filePaths.length) return { ok: false, canceled: true };
|
if (canceled || !filePaths.length) return { ok: false, canceled: true };
|
||||||
@ -1913,25 +1789,14 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
|
|||||||
_lastImportPath = sourcePath;
|
_lastImportPath = sourcePath;
|
||||||
}
|
}
|
||||||
let imported;
|
let imported;
|
||||||
const looksLikeJson = buffer.length >= 1 && (buffer[0] === 0x7B || buffer[0] === 0x20 || buffer[0] === 0x0A || buffer[0] === 0x0D || buffer[0] === 0x09 || buffer[0] === 0xEF);
|
try {
|
||||||
if (looksLikeJson) {
|
imported = backupCrypto.decrypt(buffer, legacyPassword);
|
||||||
try {
|
} catch (err) {
|
||||||
const text = buffer.toString('utf-8').replace(/^\uFEFF/, '');
|
if (err && err.needsPassword) {
|
||||||
imported = JSON.parse(text);
|
return { ok: false, needsPassword: true };
|
||||||
} catch (err) {
|
|
||||||
_lastImportPath = null;
|
|
||||||
return { ok: false, error: 'Klartext-Backup ist kein gültiges JSON: ' + (err.message || err) };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
imported = backupCrypto.decrypt(buffer, legacyPassword);
|
|
||||||
} catch (err) {
|
|
||||||
if (err && err.needsPassword) {
|
|
||||||
return { ok: false, needsPassword: true, hint: 'Falls dieses Backup mit der aktuellen Version erzeugt wurde, ist die Datei vermutlich beim Transfer beschädigt worden (z. B. FTP-Text-Modus). Versuch es mit einem Klartext-JSON-Export.' };
|
|
||||||
}
|
|
||||||
_lastImportPath = null;
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
_lastImportPath = null;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
_lastImportPath = null;
|
_lastImportPath = null;
|
||||||
// Validate imported data has required structure
|
// Validate imported data has required structure
|
||||||
@ -2049,9 +1914,13 @@ ipcMain.handle('app:check-updates', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('app:install-update', () => {
|
ipcMain.handle('app:install-update', () => {
|
||||||
installUpdate((progress) => {
|
installUpdate((progress) => {
|
||||||
safeSend('app:update-progress', progress);
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('app:update-progress', progress);
|
||||||
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
safeSend('app:update-progress', { stage: 'error', error: err.message });
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('app:update-progress', { stage: 'error', error: err.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return { started: true };
|
return { started: true };
|
||||||
});
|
});
|
||||||
@ -2130,7 +1999,9 @@ function startFolderMonitor(settings) {
|
|||||||
folderMonitor.removeAllListeners();
|
folderMonitor.removeAllListeners();
|
||||||
folderMonitor.on('new-files', (files) => {
|
folderMonitor.on('new-files', (files) => {
|
||||||
debugLog(`folder-monitor: ${files.length} new file(s)`);
|
debugLog(`folder-monitor: ${files.length} new file(s)`);
|
||||||
safeSend('folder-monitor:new-files', files);
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('folder-monitor:new-files', files);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
folderMonitor.on('error', (err) => {
|
folderMonitor.on('error', (err) => {
|
||||||
debugLog(`folder-monitor error: ${err.message}`);
|
debugLog(`folder-monitor error: ${err.message}`);
|
||||||
@ -2374,7 +2245,9 @@ ipcMain.handle('remote:get-capture-source-id', async () => {
|
|||||||
|
|
||||||
// IPC: Client count updates from capture window
|
// IPC: Client count updates from capture window
|
||||||
ipcMain.on('remote:client-count', (_event, count) => {
|
ipcMain.on('remote:client-count', (_event, count) => {
|
||||||
safeSend('remote:client-count', count);
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('remote:client-count', count);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// IPC: Remote settings
|
// IPC: Remote settings
|
||||||
@ -2479,7 +2352,7 @@ ipcMain.on('drop-target:files', (_event, paths) => {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
safeSend('drop-target:files', paths);
|
mainWindow.webContents.send('drop-target:files', paths);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2516,7 +2389,9 @@ function handleShutdownAfterFinish() {
|
|||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
// Notify renderer
|
// Notify renderer
|
||||||
safeSend('shutdown-countdown', { mode: shutdownMode, seconds: 60 });
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('shutdown-countdown', { mode: shutdownMode, seconds: 60 });
|
||||||
|
}
|
||||||
|
|
||||||
// Clear any previous countdown to prevent orphaned timers
|
// Clear any previous countdown to prevent orphaned timers
|
||||||
if (shutdownTimer) clearTimeout(shutdownTimer);
|
if (shutdownTimer) clearTimeout(shutdownTimer);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.58",
|
"version": "3.3.47",
|
||||||
"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": {
|
||||||
|
|||||||
@ -30,9 +30,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// File selection
|
// File selection
|
||||||
selectFiles: () => ipcRenderer.invoke('select-files'),
|
selectFiles: () => ipcRenderer.invoke('select-files'),
|
||||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
selectFolderWithSizes: () => ipcRenderer.invoke('select-folder-with-sizes'),
|
|
||||||
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
||||||
getFileSizes: (paths) => ipcRenderer.invoke('get-file-sizes', paths),
|
|
||||||
|
|
||||||
// Upload control
|
// Upload control
|
||||||
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
||||||
|
|||||||
216
renderer/app.js
216
renderer/app.js
@ -63,12 +63,10 @@ const queueSortState = { key: 'filename', direction: 'asc' };
|
|||||||
// History state
|
// History state
|
||||||
let historyRowsData = [];
|
let historyRowsData = [];
|
||||||
let historySortState = { key: 'date', direction: 'desc' };
|
let historySortState = { key: 'date', direction: 'desc' };
|
||||||
let _historySortClicked = false;
|
|
||||||
|
|
||||||
// Session-specific files for the "Files" panel (resets each session)
|
// Session-specific files for the "Files" panel (resets each session)
|
||||||
let sessionFilesData = [];
|
let sessionFilesData = [];
|
||||||
const recentSortState = { key: 'date', direction: 'desc' };
|
const recentSortState = { key: 'date', direction: 'desc' };
|
||||||
let _recentSortClicked = false;
|
|
||||||
const selectedRecentIds = new Set();
|
const selectedRecentIds = new Set();
|
||||||
// Maintained incrementally — avoids O(n) filter() scans every 250ms in the status bar.
|
// Maintained incrementally — avoids O(n) filter() scans every 250ms in the status bar.
|
||||||
let _sessionDoneCount = 0;
|
let _sessionDoneCount = 0;
|
||||||
@ -77,19 +75,6 @@ let _sessionErrorCount = 0;
|
|||||||
// Huge with thousands of rows × thousands of incoming results.
|
// Huge with thousands of rows × thousands of incoming results.
|
||||||
const _sessionFileKeys = new Set();
|
const _sessionFileKeys = new Set();
|
||||||
|
|
||||||
window.addEventListener('error', (e) => {
|
|
||||||
try {
|
|
||||||
const msg = `RENDERER ERROR: ${e.message} at ${e.filename}:${e.lineno}:${e.colno}${e.error && e.error.stack ? '\n' + e.error.stack : ''}`;
|
|
||||||
if (window.api && window.api.debugLog) window.api.debugLog(msg);
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
window.addEventListener('unhandledrejection', (e) => {
|
|
||||||
try {
|
|
||||||
const reason = e.reason && e.reason.stack ? e.reason.stack : (e.reason && e.reason.message) || String(e.reason);
|
|
||||||
if (window.api && window.api.debugLog) window.api.debugLog(`RENDERER UNHANDLED REJECTION: ${reason}`);
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Init ---
|
// --- Init ---
|
||||||
async function init() {
|
async function init() {
|
||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
@ -99,7 +84,6 @@ async function init() {
|
|||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
restoreQueueStateFromConfig();
|
restoreQueueStateFromConfig();
|
||||||
await _autoDeduplicateFromLog();
|
await _autoDeduplicateFromLog();
|
||||||
_hydrateMissingJobSizes();
|
|
||||||
renderHosterSummary();
|
renderHosterSummary();
|
||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
@ -696,12 +680,11 @@ async function addDroppedFiles(fileList) {
|
|||||||
const folderFiles = await window.api.resolveFolderFiles(filePath);
|
const folderFiles = await window.api.resolveFolderFiles(filePath);
|
||||||
if (folderFiles && folderFiles.length > 0) {
|
if (folderFiles && folderFiles.length > 0) {
|
||||||
for (const fp of folderFiles) {
|
for (const fp of folderFiles) {
|
||||||
const p = typeof fp === 'string' ? fp : (fp && fp.path);
|
if (!existingPaths.has(fp)) {
|
||||||
if (!p || existingPaths.has(p)) continue;
|
const name = fp.split('\\').pop().split('/').pop();
|
||||||
const name = typeof fp === 'string' ? p.split('\\').pop().split('/').pop() : (fp.name || p.split('\\').pop().split('/').pop());
|
newFiles.push({ path: fp, name, size: null });
|
||||||
const size = typeof fp === 'string' ? null : (fp.size || 0);
|
existingPaths.add(fp);
|
||||||
newFiles.push({ path: p, name, size });
|
}
|
||||||
existingPaths.add(p);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -732,52 +715,28 @@ async function pickFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pickFolder() {
|
async function pickFolder() {
|
||||||
const richFiles = window.api.selectFolderWithSizes ? await window.api.selectFolderWithSizes() : null;
|
|
||||||
if (richFiles && Array.isArray(richFiles)) { addPathsToQueue(richFiles); return; }
|
|
||||||
const paths = await window.api.selectFolder();
|
const paths = await window.api.selectFolder();
|
||||||
if (!paths) return;
|
if (!paths) return;
|
||||||
addPathsToQueue(paths);
|
addPathsToQueue(paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPathsToQueue(paths) {
|
function addPathsToQueue(paths) {
|
||||||
|
// Build path-Set once so dedup is O(1) per candidate instead of O(n+m).
|
||||||
|
// Matters when the user picks a folder with thousands of files.
|
||||||
const existing = new Set();
|
const existing = new Set();
|
||||||
for (const f of selectedFiles) existing.add(f.path);
|
for (const f of selectedFiles) existing.add(f.path);
|
||||||
for (const f of _pendingFiles) existing.add(f.path);
|
for (const f of _pendingFiles) existing.add(f.path);
|
||||||
|
|
||||||
const newFiles = [];
|
const newFiles = [];
|
||||||
const pendingSizeFetch = [];
|
for (const p of paths) {
|
||||||
for (const entry of paths) {
|
if (existing.has(p)) continue;
|
||||||
const p = typeof entry === 'string' ? entry : (entry && entry.path);
|
|
||||||
if (!p || existing.has(p)) continue;
|
|
||||||
existing.add(p);
|
existing.add(p);
|
||||||
const name = typeof entry === 'string' ? p.split('\\').pop().split('/').pop() : (entry.name || p.split('\\').pop().split('/').pop());
|
const name = p.split('\\').pop().split('/').pop();
|
||||||
const size = typeof entry === 'string' ? null : (entry.size || 0);
|
newFiles.push({ path: p, name, size: null });
|
||||||
newFiles.push({ path: p, name, size });
|
|
||||||
if (size === null || size === undefined || size === 0) pendingSizeFetch.push(p);
|
|
||||||
}
|
}
|
||||||
if (newFiles.length > 0) {
|
if (newFiles.length > 0) {
|
||||||
_pendingFiles.push(...newFiles);
|
_pendingFiles.push(...newFiles);
|
||||||
openHosterModal();
|
openHosterModal();
|
||||||
if (pendingSizeFetch.length > 0 && window.api.getFileSizes) {
|
|
||||||
window.api.getFileSizes(pendingSizeFetch).then((sizeMap) => {
|
|
||||||
if (!sizeMap || typeof sizeMap !== 'object') return;
|
|
||||||
let changed = false;
|
|
||||||
for (const f of _pendingFiles) {
|
|
||||||
if (sizeMap[f.path] && (!f.size || f.size === 0)) { f.size = sizeMap[f.path]; changed = true; }
|
|
||||||
}
|
|
||||||
for (const f of selectedFiles) {
|
|
||||||
if (sizeMap[f.path] && (!f.size || f.size === 0)) { f.size = sizeMap[f.path]; changed = true; }
|
|
||||||
}
|
|
||||||
for (const j of queueJobs) {
|
|
||||||
if (sizeMap[j.file] && (!j.bytesTotal || j.bytesTotal === 0)) { j.bytesTotal = sizeMap[j.file]; changed = true; }
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
_queueStatsCache = null;
|
|
||||||
if (typeof renderQueueTable === 'function') renderQueueTable();
|
|
||||||
if (typeof updateStatusBar === 'function') updateStatusBar();
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -998,54 +957,12 @@ function scheduleStatusChangeUpdate() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _hydrateMissingJobSizes(jobsLike) {
|
|
||||||
if (!window.api || !window.api.getFileSizes) return;
|
|
||||||
const paths = [];
|
|
||||||
const seen = new Set();
|
|
||||||
const source = Array.isArray(jobsLike) ? jobsLike : queueJobs;
|
|
||||||
for (const j of source) {
|
|
||||||
if (!j || !j.file) continue;
|
|
||||||
if (j.bytesTotal && j.bytesTotal > 0) continue;
|
|
||||||
if (seen.has(j.file)) continue;
|
|
||||||
seen.add(j.file);
|
|
||||||
paths.push(j.file);
|
|
||||||
}
|
|
||||||
if (paths.length === 0) return;
|
|
||||||
window.api.getFileSizes(paths).then((sizeMap) => {
|
|
||||||
if (!sizeMap || typeof sizeMap !== 'object') return;
|
|
||||||
let changed = false;
|
|
||||||
for (const j of queueJobs) {
|
|
||||||
if (sizeMap[j.file] && (!j.bytesTotal || j.bytesTotal === 0)) {
|
|
||||||
j.bytesTotal = sizeMap[j.file];
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const f of selectedFiles) {
|
|
||||||
if (sizeMap[f.path] && (!f.size || f.size === 0)) f.size = sizeMap[f.path];
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
_queueStatsCache = null;
|
|
||||||
if (typeof renderQueueTable === 'function') renderQueueTable();
|
|
||||||
if (typeof updateStatusBar === 'function') updateStatusBar();
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _formatUploadedSize(job) {
|
|
||||||
const bt = job.bytesTotal || 0;
|
|
||||||
const bu = job.bytesUploaded || 0;
|
|
||||||
const s = job.status;
|
|
||||||
if (s === 'preview') return bt > 0 ? formatSize(bt) : '...';
|
|
||||||
if (s === 'queued' || s === 'getting-server' || s === 'retrying') {
|
|
||||||
return bt > 0 ? `${formatSize(bu)} / ${formatSize(bt)}` : '...';
|
|
||||||
}
|
|
||||||
return `${formatSize(bu)} / ${formatSize(bt)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRowHtml(job) {
|
function buildRowHtml(job) {
|
||||||
const statusClass = `status-${job.status}`;
|
const statusClass = `status-${job.status}`;
|
||||||
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
||||||
const uploadedSize = _formatUploadedSize(job);
|
const uploadedSize = job.status === 'preview'
|
||||||
|
? (job.bytesTotal > 0 ? formatSize(job.bytesTotal) : '...')
|
||||||
|
: `${formatSize(job.bytesUploaded)} / ${formatSize(job.bytesTotal)}`;
|
||||||
const statusText = getStatusText(job);
|
const statusText = getStatusText(job);
|
||||||
const elapsed = formatTime(job.elapsed);
|
const elapsed = formatTime(job.elapsed);
|
||||||
const remaining = formatTime(job.remaining);
|
const remaining = formatTime(job.remaining);
|
||||||
@ -1075,7 +992,9 @@ function buildRowHtml(job) {
|
|||||||
// In-place update of a single row's cells (avoids full innerHTML rebuild)
|
// In-place update of a single row's cells (avoids full innerHTML rebuild)
|
||||||
function _updateRowInPlace(tr, job) {
|
function _updateRowInPlace(tr, job) {
|
||||||
const statusClass = `status-${job.status}`;
|
const statusClass = `status-${job.status}`;
|
||||||
const uploadedSize = _formatUploadedSize(job);
|
const uploadedSize = job.status === 'preview'
|
||||||
|
? (job.bytesTotal > 0 ? formatSize(job.bytesTotal) : '...')
|
||||||
|
: `${formatSize(job.bytesUploaded)} / ${formatSize(job.bytesTotal)}`;
|
||||||
const statusText = getStatusText(job);
|
const statusText = getStatusText(job);
|
||||||
const elapsed = formatTime(job.elapsed);
|
const elapsed = formatTime(job.elapsed);
|
||||||
const remaining = formatTime(job.remaining);
|
const remaining = formatTime(job.remaining);
|
||||||
@ -1524,7 +1443,7 @@ async function doBackupExport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function askLegacyBackupPassword(hint) {
|
function askLegacyBackupPassword() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'modal-overlay';
|
overlay.className = 'modal-overlay';
|
||||||
@ -1537,7 +1456,7 @@ function askLegacyBackupPassword(hint) {
|
|||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.className = 'modal-header';
|
header.className = 'modal-header';
|
||||||
const h3 = document.createElement('h3');
|
const h3 = document.createElement('h3');
|
||||||
h3.textContent = 'Backup nicht entschlüsselbar';
|
h3.textContent = 'Passwort erforderlich';
|
||||||
header.appendChild(h3);
|
header.appendChild(h3);
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
@ -1545,15 +1464,7 @@ function askLegacyBackupPassword(hint) {
|
|||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.style.margin = '0 0 10px';
|
p.style.margin = '0 0 10px';
|
||||||
p.style.fontSize = '13px';
|
p.style.fontSize = '13px';
|
||||||
p.textContent = 'Wenn das Backup mit der alten Passwort-Option (vor v3.0) erstellt wurde, hier eingeben.';
|
p.textContent = 'Dieses Backup wurde mit einem Passwort verschlüsselt.';
|
||||||
if (hint) {
|
|
||||||
const p2 = document.createElement('p');
|
|
||||||
p2.style.margin = '0 0 10px';
|
|
||||||
p2.style.fontSize = '12px';
|
|
||||||
p2.style.color = 'var(--text-dim)';
|
|
||||||
p2.textContent = hint;
|
|
||||||
body.appendChild(p2);
|
|
||||||
}
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'password';
|
input.type = 'password';
|
||||||
input.className = 'key-input';
|
input.className = 'key-input';
|
||||||
@ -1597,7 +1508,7 @@ async function doBackupImport(legacyPassword) {
|
|||||||
const result = await window.api.importBackup(pw);
|
const result = await window.api.importBackup(pw);
|
||||||
if (!result || result.canceled) return;
|
if (!result || result.canceled) return;
|
||||||
if (result.needsPassword) {
|
if (result.needsPassword) {
|
||||||
const entered = await askLegacyBackupPassword(result.hint);
|
const entered = await askLegacyBackupPassword();
|
||||||
if (entered) doBackupImport(entered);
|
if (entered) doBackupImport(entered);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1769,7 +1680,6 @@ async function startUpload() {
|
|||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
uploading = true; // set immediately to prevent double-click race
|
uploading = true; // set immediately to prevent double-click race
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
_hydrateMissingJobSizes();
|
|
||||||
|
|
||||||
const hosters = getSelectedHosters();
|
const hosters = getSelectedHosters();
|
||||||
if (queueJobs.length === 0 && selectedFiles.length > 0) {
|
if (queueJobs.length === 0 && selectedFiles.length > 0) {
|
||||||
@ -1842,13 +1752,10 @@ function _markSkippedJobs(result) {
|
|||||||
|
|
||||||
async function startSelectedUpload() {
|
async function startSelectedUpload() {
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
_hydrateMissingJobSizes();
|
// Batch already running — add selected jobs (queued/error/aborted/skipped) to running batch
|
||||||
const addable = queueJobs.filter(j => selectedJobIds.has(j.id) && isStartableQueueStatus(j.status));
|
// Upload-manager has duplicate protection (skips jobs already tracked)
|
||||||
if (addable.length === 0) {
|
const addable = queueJobs.filter(j => selectedJobIds.has(j.id) && ['queued', 'error', 'aborted', 'skipped'].includes(j.status));
|
||||||
if (selectedJobIds.size > 0) showCopyToast('Keine startbaren Jobs ausgewählt (alle laufen schon oder sind fertig).');
|
if (addable.length > 0) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
addable.forEach(j => {
|
addable.forEach(j => {
|
||||||
j.status = 'queued'; j.error = null; j.result = null;
|
j.status = 'queued'; j.error = null; j.result = null;
|
||||||
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
||||||
@ -1893,6 +1800,7 @@ async function startSelectedUpload() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
uploading = true; // set immediately to prevent double-click race
|
uploading = true; // set immediately to prevent double-click race
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
@ -1965,14 +1873,6 @@ async function cancelUpload() {
|
|||||||
|
|
||||||
// --- Progress handling ---
|
// --- Progress handling ---
|
||||||
function handleProgress(data) {
|
function handleProgress(data) {
|
||||||
try {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
_handleProgressImpl(data);
|
|
||||||
} catch (err) {
|
|
||||||
if (window.api && window.api.debugLog) window.api.debugLog(`handleProgress error: ${err && err.stack ? err.stack : err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function _handleProgressImpl(data) {
|
|
||||||
let job = data.jobId ? _jobIndexById.get(data.jobId) : null;
|
let job = data.jobId ? _jobIndexById.get(data.jobId) : null;
|
||||||
if (!job && data.uploadId) job = _jobIndexByUploadId.get(data.uploadId);
|
if (!job && data.uploadId) job = _jobIndexByUploadId.get(data.uploadId);
|
||||||
if (!job) {
|
if (!job) {
|
||||||
@ -2152,6 +2052,7 @@ function handleBatchDone(summary) {
|
|||||||
|
|
||||||
lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 };
|
lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 };
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
|
_maybeShowBatchSummary(summary);
|
||||||
_refreshSessionFailedSnapshot();
|
_refreshSessionFailedSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2233,14 +2134,6 @@ function _retryFailedFromBuckets(buckets, transientOnly) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleStats(data) {
|
function handleStats(data) {
|
||||||
try {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
_handleStatsImpl(data);
|
|
||||||
} catch (err) {
|
|
||||||
if (window.api && window.api.debugLog) window.api.debugLog(`handleStats error: ${err && err.stack ? err.stack : err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function _handleStatsImpl(data) {
|
|
||||||
lastUploadStats = {
|
lastUploadStats = {
|
||||||
state: data.state || 'idle',
|
state: data.state || 'idle',
|
||||||
globalSpeedKbs: data.globalSpeedKbs || 0,
|
globalSpeedKbs: data.globalSpeedKbs || 0,
|
||||||
@ -2568,7 +2461,7 @@ function _computeQueueStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_queueStatsCache = { total, remaining, inProgress, done, errors, bytesRemaining, totalSize, remainingSize, inProgressBytes };
|
_queueStatsCache = { total, remaining, inProgress, done, errors, bytesRemaining, totalSize, remainingSize, inProgressBytes };
|
||||||
(typeof queueMicrotask === 'function' ? queueMicrotask : (fn) => Promise.resolve().then(fn))(() => { _queueStatsCache = null; });
|
queueMicrotask(() => { _queueStatsCache = null; });
|
||||||
return _queueStatsCache;
|
return _queueStatsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2629,10 +2522,8 @@ async function executeHealthCheck(hosters, _mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
||||||
if (healthCheckRunning) {
|
if (healthCheckRunning || (uploading && mode === 'manual')) return [];
|
||||||
if (mode === 'manual') showCopyToast('Account-Check läuft bereits.');
|
// Build check list: all enabled accounts with creds
|
||||||
return [];
|
|
||||||
}
|
|
||||||
let hosters;
|
let hosters;
|
||||||
if (Array.isArray(requestedHosters) && requestedHosters.length > 0) {
|
if (Array.isArray(requestedHosters) && requestedHosters.length > 0) {
|
||||||
hosters = requestedHosters;
|
hosters = requestedHosters;
|
||||||
@ -3759,10 +3650,7 @@ async function deleteAccount(accountId) {
|
|||||||
// Fire-and-forget the persist. The earlier `await getConfig()` round-trip
|
// Fire-and-forget the persist. The earlier `await getConfig()` round-trip
|
||||||
// was redundant (we already have the truth in memory) and was the main
|
// was redundant (we already have the truth in memory) and was the main
|
||||||
// source of perceived lag on add/delete.
|
// source of perceived lag on add/delete.
|
||||||
window.api.saveConfig({ hosters: config.hosters }).catch((err) => {
|
window.api.saveConfig({ hosters: config.hosters }).catch(() => {});
|
||||||
if (window.api && window.api.debugLog) window.api.debugLog(`deleteAccount saveConfig failed: ${err && err.message ? err.message : err}`);
|
|
||||||
showCopyToast('Account-Löschung konnte nicht persistiert werden — bitte erneut versuchen.');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readAccountCredsFromModal(authType) {
|
function readAccountCredsFromModal(authType) {
|
||||||
@ -3968,15 +3856,6 @@ async function _commitAccount(ctx, creds, validatedStatus, validatedMessage) {
|
|||||||
const idx = config.hosters[ctx.hosterName].findIndex(a => a.id === accountId);
|
const idx = config.hosters[ctx.hosterName].findIndex(a => a.id === accountId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
config.hosters[ctx.hosterName][idx] = { ...config.hosters[ctx.hosterName][idx], ...creds };
|
config.hosters[ctx.hosterName][idx] = { ...config.hosters[ctx.hosterName][idx], ...creds };
|
||||||
} else {
|
|
||||||
_accountModalBusy = false;
|
|
||||||
const _sb = document.getElementById('saveAccountBtn'); if (_sb) _sb.disabled = false;
|
|
||||||
const _st = document.getElementById('accountModalStatus');
|
|
||||||
if (_st) {
|
|
||||||
_st.textContent = 'Account nicht mehr in der Config — wurde extern gelöscht. Modal schließen und neu anlegen.';
|
|
||||||
_st.className = 'account-modal-status error';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
accountId = `${ctx.hosterName}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
accountId = `${ctx.hosterName}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
@ -4261,14 +4140,8 @@ function renderHistoryTable(container) {
|
|||||||
const th = e.target.closest('th.sortable');
|
const th = e.target.closest('th.sortable');
|
||||||
if (th && container.contains(th)) {
|
if (th && container.contains(th)) {
|
||||||
const key = th.dataset.historySort;
|
const key = th.dataset.historySort;
|
||||||
const defaultDir = key === 'date' ? 'desc' : 'asc';
|
if (historySortState.key === key) historySortState.direction = historySortState.direction === 'asc' ? 'desc' : 'asc';
|
||||||
if (!_historySortClicked || historySortState.key !== key) {
|
else { historySortState.key = key; historySortState.direction = key === 'date' ? 'desc' : 'asc'; }
|
||||||
_historySortClicked = true;
|
|
||||||
historySortState.key = key;
|
|
||||||
historySortState.direction = defaultDir;
|
|
||||||
} else {
|
|
||||||
historySortState.direction = historySortState.direction === 'asc' ? 'desc' : 'asc';
|
|
||||||
}
|
|
||||||
renderHistoryTable(container);
|
renderHistoryTable(container);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -4324,13 +4197,11 @@ function setupListeners() {
|
|||||||
const th = e.target.closest('th[data-recent-sort]');
|
const th = e.target.closest('th[data-recent-sort]');
|
||||||
if (!th) return;
|
if (!th) return;
|
||||||
const key = th.dataset.recentSort;
|
const key = th.dataset.recentSort;
|
||||||
const defaultDir = key === 'date' ? 'desc' : 'asc';
|
if (recentSortState.key === key) {
|
||||||
if (!_recentSortClicked || recentSortState.key !== key) {
|
|
||||||
_recentSortClicked = true;
|
|
||||||
recentSortState.key = key;
|
|
||||||
recentSortState.direction = defaultDir;
|
|
||||||
} else {
|
|
||||||
recentSortState.direction = recentSortState.direction === 'desc' ? 'asc' : 'desc';
|
recentSortState.direction = recentSortState.direction === 'desc' ? 'asc' : 'desc';
|
||||||
|
} else {
|
||||||
|
recentSortState.key = key;
|
||||||
|
recentSortState.direction = key === 'date' ? 'desc' : 'asc';
|
||||||
}
|
}
|
||||||
renderRecentUploadsPanel();
|
renderRecentUploadsPanel();
|
||||||
});
|
});
|
||||||
@ -4924,15 +4795,4 @@ function updateStatsPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Start ---
|
// --- Start ---
|
||||||
init().catch((err) => {
|
init();
|
||||||
try {
|
|
||||||
if (window.api && window.api.debugLog) window.api.debugLog(`init failed: ${err && err.stack ? err.stack : err}`);
|
|
||||||
const root = document.getElementById('app') || document.body;
|
|
||||||
if (root) {
|
|
||||||
const banner = document.createElement('div');
|
|
||||||
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#5a1e1e;color:#fff;padding:8px;z-index:99999;font-family:sans-serif;font-size:13px';
|
|
||||||
banner.textContent = 'Initialisierung fehlgeschlagen: ' + (err && err.message ? err.message : err) + ' — bitte Diagnose-Paket exportieren oder Programm neu starten.';
|
|
||||||
root.appendChild(banner);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
|
|||||||
@ -342,6 +342,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="batchSummaryModal" style="display:none">
|
||||||
|
<div class="modal-content" style="max-width:680px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Batch-Zusammenfassung</h2>
|
||||||
|
<button class="icon-btn" id="batchSummaryClose" aria-label="Schließen">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="batchSummaryList"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" id="batchSummaryRetryTransient">Transiente erneut hochladen</button>
|
||||||
|
<button class="btn btn-primary" id="batchSummaryRetryAll">Alle Fehler erneut versuchen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="../lib/queue-prune.js"></script>
|
<script src="../lib/queue-prune.js"></script>
|
||||||
<script src="../lib/queue-dedup.js"></script>
|
<script src="../lib/queue-dedup.js"></script>
|
||||||
<script src="../lib/log-mode.js"></script>
|
<script src="../lib/log-mode.js"></script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user