Compare commits

..

No commits in common. "d53eea443eead69fa27aaeb8d59c3a6f86552762" and "052bd940f1fd8698c8ef51fced3c9f06e2ff6e9e" have entirely different histories.

8 changed files with 363 additions and 736 deletions

View File

@ -10,35 +10,12 @@ const HOSTER_SETTINGS_DEFAULTS = {
maxSizeMb: 0 // 0 = unlimited maxSizeMb: 0 // 0 = unlimited
}; };
// Template for each hoster type (used as defaults for new accounts)
const HOSTER_ACCOUNT_TEMPLATES = {
'doodstream.com': { enabled: true, authType: 'login', username: '', password: '' },
'doodstream.com:api': { enabled: true, authType: 'api', apiKey: '' },
'voe.sx': { enabled: true, authType: 'login', username: '', password: '' },
'voe.sx:api': { enabled: true, authType: 'api', apiKey: '' },
'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' },
'byse.sx': { enabled: true, authType: 'api', apiKey: '' }
};
// All known hoster names (used for iteration)
const HOSTER_NAMES = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'];
// Dropdown options for "Add Account" modal: value -> label
const HOSTER_ADD_OPTIONS = [
{ value: 'doodstream.com', label: 'Doodstream (Web Login)', hoster: 'doodstream.com', authType: 'login' },
{ value: 'doodstream.com:api', label: 'Doodstream (API)', hoster: 'doodstream.com', authType: 'api' },
{ value: 'voe.sx', label: 'Voe (Web Login)', hoster: 'voe.sx', authType: 'login' },
{ value: 'voe.sx:api', label: 'Voe (API)', hoster: 'voe.sx', authType: 'api' },
{ value: 'vidmoly.me', label: 'Vidmoly (Web Login)', hoster: 'vidmoly.me', authType: 'login' },
{ value: 'byse.sx', label: 'Byse (API)', hoster: 'byse.sx', authType: 'api' }
];
const DEFAULTS = { const DEFAULTS = {
hosters: { hosters: {
'doodstream.com': [], 'doodstream.com': { enabled: true, apiKey: '', username: '', password: '' },
'voe.sx': [], 'voe.sx': { enabled: true, apiKey: '' },
'vidmoly.me': [], 'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' },
'byse.sx': [] 'byse.sx': { enabled: true, apiKey: '' }
}, },
hosterSettings: { hosterSettings: {
'doodstream.com': { ...HOSTER_SETTINGS_DEFAULTS }, 'doodstream.com': { ...HOSTER_SETTINGS_DEFAULTS },
@ -136,46 +113,13 @@ class ConfigStore {
} }
if (!data) return JSON.parse(JSON.stringify(DEFAULTS)); if (!data) return JSON.parse(JSON.stringify(DEFAULTS));
// Migrate old single-object format to array format // Merge with defaults so new hosters are always present
const hosters = { ...DEFAULTS.hosters };
for (const [name, val] of Object.entries(data.hosters || {})) { for (const [name, val] of Object.entries(data.hosters || {})) {
if (val && !Array.isArray(val)) { if (hosters[name]) {
if (!val.id) val.id = `${name}-migrated-${Date.now()}`; hosters[name] = { ...hosters[name], ...val };
// Infer authType for old format accounts
if (!val.authType) {
if (name === 'byse.sx') val.authType = 'api';
else if (name === 'vidmoly.me') val.authType = 'login';
else if (val.username && val.password) val.authType = 'login';
else if (val.apiKey) val.authType = 'api';
else val.authType = 'login';
}
data.hosters[name] = [val];
} }
} }
// Merge hosters: ensure all known hosters exist as arrays
const hosters = {};
for (const name of HOSTER_NAMES) {
const saved = data.hosters && data.hosters[name];
if (Array.isArray(saved) && saved.length > 0) {
hosters[name] = saved.map((acc, i) => {
// Ensure authType is set on every account
if (!acc.authType) {
if (name === 'byse.sx') acc.authType = 'api';
else if (name === 'vidmoly.me') acc.authType = 'login';
else if (acc.username && acc.password) acc.authType = 'login';
else if (acc.apiKey) acc.authType = 'api';
else acc.authType = 'login';
}
return {
...acc,
id: acc.id || `${name}-${Date.now()}-${i}`
};
});
} else {
hosters[name] = [];
}
}
// Merge hoster settings with defaults // Merge hoster settings with defaults
const hosterSettings = {}; const hosterSettings = {};
for (const name of Object.keys(DEFAULTS.hosterSettings)) { for (const name of Object.keys(DEFAULTS.hosterSettings)) {
@ -252,6 +196,3 @@ class ConfigStore {
} }
module.exports = ConfigStore; module.exports = ConfigStore;
module.exports.HOSTER_ACCOUNT_TEMPLATES = HOSTER_ACCOUNT_TEMPLATES;
module.exports.HOSTER_NAMES = HOSTER_NAMES;
module.exports.HOSTER_ADD_OPTIONS = HOSTER_ADD_OPTIONS;

View File

@ -37,12 +37,6 @@ class UploadManager extends EventEmitter {
this.lastStartTime = {}; // hoster -> timestamp of last upload start this.lastStartTime = {}; // hoster -> timestamp of last upload start
this.intervalLocks = {}; // hoster -> Promise chain for serialized interval waits this.intervalLocks = {}; // hoster -> Promise chain for serialized interval waits
this.globalThrottle = null; this.globalThrottle = null;
this._failedAccounts = new Map(); // hoster -> Set of failed accountIds
this._accountOverrides = new Map(); // hoster -> fallback account object
}
switchAccount(hoster, fallbackAccount) {
this._accountOverrides.set(hoster, fallbackAccount);
} }
updateSettings(hosterSettings, globalSettings) { updateSettings(hosterSettings, globalSettings) {
@ -370,7 +364,22 @@ class UploadManager extends EventEmitter {
}); });
}; };
const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle); let result;
if (task.hoster === 'vidmoly.me' && task.username) {
const vidmoly = new VidmolyUploader();
await vidmoly.login(task.username, task.password);
result = await vidmoly.upload(task.file, progressCb, uploadSignalBundle.signal, throttle);
} else if (task.hoster === 'voe.sx' && task.username) {
const voe = new VoeUploader();
await voe.login(task.username, task.password);
result = await voe.upload(task.file, progressCb, uploadSignalBundle.signal, throttle);
} else if (task.hoster === 'doodstream.com' && task.username) {
const dood = new DoodstreamUploader();
await dood.login(task.username, task.password);
result = await dood.upload(task.file, progressCb, uploadSignalBundle.signal, throttle);
} else {
result = await uploadFile(task.hoster, task.file, task.apiKey, progressCb, uploadSignalBundle.signal, throttle);
}
const elapsed = Math.round((Date.now() - jobStart) / 1000); const elapsed = Math.round((Date.now() - jobStart) / 1000);
this.sessionBytes += fileSize; this.sessionBytes += fileSize;
@ -423,83 +432,6 @@ class UploadManager extends EventEmitter {
return; return;
} }
// Account fallback: if this account hasn't failed before, try switching
if (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
this._failedAccounts.set(task.hoster + ':' + task.accountId, true);
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
// Wait briefly for switchAccount() to be called from main process
await this._sleep(800, signal);
const override = this._accountOverrides.get(task.hoster);
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
// Switch to fallback account and retry this file
task.accountId = override.id;
task.username = override.username;
task.password = override.password;
task.apiKey = override.apiKey;
this._emitProgress(uploadId, fileName, task.hoster, {
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
speedKbs: 0, elapsed: 0, remaining: 0,
error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts
});
// Re-run retry loop with new account
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (signal.aborted || this.stopAfterActive) break;
if (attempt > 1) {
this._emitProgress(uploadId, fileName, task.hoster, {
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
speedKbs: 0, elapsed: 0, remaining: 0,
error: lastError ? lastError.message : '', result: null, attempt, maxAttempts
});
await this._sleep(3000, signal);
}
try {
const jobStart = Date.now();
let lastBytes = 0;
let lastSpeedTime = jobStart;
let currentSpeedKbs = 0;
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
const progressCb = (bytesUploaded, bytesTotal) => {
const now = Date.now();
const timeDelta = (now - lastSpeedTime) / 1000;
if (timeDelta >= 1) {
currentSpeedKbs = Math.round((bytesUploaded - lastBytes) / timeDelta / 1024);
lastBytes = bytesUploaded;
lastSpeedTime = now;
}
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
const elapsed = Math.round((now - jobStart) / 1000);
const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0;
this._emitProgress(uploadId, fileName, task.hoster, {
jobId, status: 'uploading',
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
bytesUploaded, bytesTotal, speedKbs: currentSpeedKbs,
elapsed, remaining, error: null, result: null, attempt, maxAttempts
});
};
const hosterThrottle = settings.maxSpeedKbs > 0 ? new Throttle(settings.maxSpeedKbs * 1024) : null;
const globalThrottle = this._getGlobalThrottle();
const throttle = hosterThrottle && globalThrottle
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
: hosterThrottle || globalThrottle;
const result = await this._executeUpload(task, progressCb, signal, throttle);
this.activeJobs.delete(uploadId);
this.sessionBytes += fileSize;
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
recordFinalResult('done', { result });
return;
} catch (err) {
this.activeJobs.delete(uploadId);
lastError = err;
if (signal.aborted || this.stopAfterActive) break;
if (attempt >= maxAttempts) break;
}
}
}
}
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler'; const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
emitFinalStatus('error', { error }); emitFinalStatus('error', { error });
recordFinalResult('error', { error }); recordFinalResult('error', { error });
@ -521,24 +453,6 @@ class UploadManager extends EventEmitter {
} }
} }
async _executeUpload(task, progressCb, signal, throttle) {
if (task.hoster === 'vidmoly.me' && task.username) {
const vidmoly = new VidmolyUploader();
await vidmoly.login(task.username, task.password);
return vidmoly.upload(task.file, progressCb, signal, throttle);
} else if (task.hoster === 'voe.sx' && task.username) {
const voe = new VoeUploader();
await voe.login(task.username, task.password);
return voe.upload(task.file, progressCb, signal, throttle);
} else if (task.hoster === 'doodstream.com' && task.username) {
const dood = new DoodstreamUploader();
await dood.login(task.username, task.password);
return dood.upload(task.file, progressCb, signal, throttle);
} else {
return uploadFile(task.hoster, task.file, task.apiKey, progressCb, signal, throttle);
}
}
_emitProgress(uploadId, fileName, hoster, data) { _emitProgress(uploadId, fileName, hoster, data) {
this.emit('progress', { uploadId, fileName, hoster, ...data }); this.emit('progress', { uploadId, fileName, hoster, ...data });
} }

243
main.js
View File

@ -115,68 +115,82 @@ function appendUploadLog(hoster, link, fileName) {
} catch {} } catch {}
} }
// --- Multi-account helpers ---
function hosterAccountHasCreds(name, account) {
if (!account) return false;
if (account.authType === 'api') return !!account.apiKey;
if (account.authType === 'login') return !!(account.username && account.password);
// Fallback for old format
if (name === 'vidmoly.me') return !!(account.username && account.password);
if (name === 'voe.sx' || name === 'doodstream.com') return !!(account.username && account.password) || !!account.apiKey;
return !!account.apiKey;
}
function getPrimaryAccount(config, hosterName) {
const accounts = config.hosters[hosterName];
if (!Array.isArray(accounts)) return null;
return accounts.find(a => a.enabled !== false && hosterAccountHasCreds(hosterName, a)) || null;
}
function getNextFallbackAccount(config, hosterName, failedAccountId) {
const accounts = config.hosters[hosterName];
if (!Array.isArray(accounts)) return null;
const failedIndex = accounts.findIndex(a => a.id === failedAccountId);
if (failedIndex < 0) return null;
for (let i = failedIndex + 1; i < accounts.length; i++) {
if (accounts[i].enabled !== false && hosterAccountHasCreds(hosterName, accounts[i])) {
return accounts[i];
}
}
return null;
}
function buildTaskFromAccount(hoster, account, extra) {
const task = { ...extra, hoster, accountId: account.id };
if (account.authType === 'api' && account.apiKey) {
task.apiKey = account.apiKey;
} else if (account.username && account.password) {
task.username = account.username;
task.password = account.password;
} else if (account.apiKey) {
task.apiKey = account.apiKey;
}
return task;
}
function buildUploadTasks(config, files, hosters) { function buildUploadTasks(config, files, hosters) {
const tasks = []; const tasks = [];
for (const file of files) { for (const file of files) {
for (const hoster of hosters) { for (const hoster of hosters) {
const account = getPrimaryAccount(config, hoster); const hosterConfig = config.hosters[hoster];
if (!account) { debugLog(` skip ${hoster}: no enabled account with creds`); continue; } if (!hosterConfig) {
tasks.push(buildTaskFromAccount(hoster, account, { file })); debugLog(` skip ${hoster}: no config`);
continue;
}
if (hoster === 'vidmoly.me') {
if (!hosterConfig.username || !hosterConfig.password) {
debugLog(` skip ${hoster}: missing username/password`);
continue;
}
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
} else if (hoster === 'voe.sx' && hosterConfig.username && hosterConfig.password) {
// VOE login-based upload (preferred over API)
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
debugLog(` task: ${hoster} login=${hosterConfig.username.slice(0, 6)}...`);
} else if (hoster === 'doodstream.com' && hosterConfig.username && hosterConfig.password) {
// Doodstream login-based upload (preferred over API)
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
debugLog(` task: ${hoster} login=${hosterConfig.username.slice(0, 6)}...`);
} else {
if (!hosterConfig.apiKey) {
debugLog(` skip ${hoster}: missing apiKey`);
continue;
}
tasks.push({ file, hoster, apiKey: hosterConfig.apiKey });
debugLog(` task: ${hoster} key=${hosterConfig.apiKey.slice(0, 6)}...`);
}
} }
} }
return tasks; return tasks;
} }
function buildUploadTasksFromJobs(config, jobs) { function buildUploadTasksFromJobs(config, jobs) {
if (!Array.isArray(jobs)) return []; if (!Array.isArray(jobs)) return [];
return jobs.flatMap((job) => { return jobs.flatMap((job) => {
if (!job || !job.file || !job.hoster) return []; if (!job || !job.file || !job.hoster) return [];
const account = getPrimaryAccount(config, job.hoster); const hosterConfig = config.hosters[job.hoster];
if (!account) { debugLog(` skip ${job.hoster}: no enabled account`); return []; } if (!hosterConfig) {
return [buildTaskFromAccount(job.hoster, account, { file: job.file, jobId: job.id || job.jobId || null })]; debugLog(` skip ${job.hoster}: no config for queued job`);
return [];
}
const baseTask = {
jobId: job.id || job.jobId || null,
file: job.file,
hoster: job.hoster
};
if (job.hoster === 'vidmoly.me') {
if (!hosterConfig.username || !hosterConfig.password) {
debugLog(` skip ${job.hoster}: missing username/password`);
return [];
}
return [{ ...baseTask, username: hosterConfig.username, password: hosterConfig.password }];
}
if ((job.hoster === 'voe.sx' || job.hoster === 'doodstream.com') && hosterConfig.username && hosterConfig.password) {
debugLog(` task: ${job.hoster} queued login=${hosterConfig.username.slice(0, 6)}...`);
return [{ ...baseTask, username: hosterConfig.username, password: hosterConfig.password, apiKey: hosterConfig.apiKey || '' }];
}
if (!hosterConfig.apiKey) {
debugLog(` skip ${job.hoster}: missing apiKey`);
return [];
}
debugLog(` task: ${job.hoster} queued key=${hosterConfig.apiKey.slice(0, 6)}...`);
return [{ ...baseTask, apiKey: hosterConfig.apiKey }];
}); });
} }
@ -346,13 +360,6 @@ async function checkByseHealth(hosterConfig) {
} }
const msg = String(serverPayload.msg || serverPayload.message || '').trim(); const msg = String(serverPayload.msg || serverPayload.message || '').trim();
// Byse API returns { msg: "OK", result: <server-url> } on success.
// If msg is "OK" but result wasn't a valid URL, treat as success with warning.
if (/^ok$/i.test(msg)) {
return { status: 'ok', message: 'API Key gültig' };
}
if (msg) { if (msg) {
return { status: 'error', message: msg }; return { status: 'error', message: msg };
} }
@ -360,69 +367,75 @@ async function checkByseHealth(hosterConfig) {
return { status: 'error', message: 'API Key ungültig oder Server nicht erreichbar' }; return { status: 'error', message: 'API Key ungültig oder Server nicht erreichbar' };
} }
// requestedChecks can be: async function runHosterHealthCheck(config, requestedHosters) {
// - array of strings (hoster names) for legacy/all-accounts check
// - array of { hoster, accountId } for specific account checks
async function runHosterHealthCheck(config, requestedChecks) {
const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx']; const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'];
const source = Array.isArray(requestedHosters) && requestedHosters.length > 0
? requestedHosters
: allowed;
// Normalize input to [{ hoster, accountId? }] const hosters = source
let checks; .map((name) => String(name || '').trim())
if (!Array.isArray(requestedChecks) || requestedChecks.length === 0) { .filter((name, index, arr) => name && arr.indexOf(name) === index);
// Check all accounts for all hosters
checks = [];
for (const name of allowed) {
const accounts = config.hosters[name];
if (Array.isArray(accounts)) {
for (const acc of accounts) {
if (hosterAccountHasCreds(name, acc)) checks.push({ hoster: name, accountId: acc.id });
}
}
}
} else if (typeof requestedChecks[0] === 'string') {
// Legacy: array of hoster names — check all accounts for each
checks = [];
for (const name of requestedChecks) {
const accounts = config.hosters[name];
if (Array.isArray(accounts)) {
for (const acc of accounts) {
if (hosterAccountHasCreds(name, acc)) checks.push({ hoster: name, accountId: acc.id });
}
}
}
} else {
checks = requestedChecks;
}
const results = await Promise.all(checks.map(async ({ hoster, accountId }) => { const checks = hosters.map(async (hoster) => {
if (!allowed.includes(hoster)) { if (!allowed.includes(hoster)) {
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' }; return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
} }
// Find specific account const hosterConfig = config && config.hosters ? config.hosters[hoster] : null;
const accounts = config.hosters[hoster];
const hosterConfig = Array.isArray(accounts) ? accounts.find(a => a.id === accountId) : null;
try { try {
let result;
if (hoster === 'doodstream.com') { if (hoster === 'doodstream.com') {
result = await withTimeout(checkDoodstreamHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Doodstream-Check'); const result = await withTimeout(
} else if (hoster === 'vidmoly.me') { checkDoodstreamHealth(hosterConfig),
result = await withTimeout(checkVidmolyHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Vidmoly-Check'); HEALTH_CHECK_TIMEOUT,
} else if (hoster === 'voe.sx') { 'Doodstream-Check'
result = await withTimeout(checkVoeHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'VOE-Check'); );
} else if (hoster === 'byse.sx') { return { hoster, ...result };
result = await withTimeout(checkByseHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Byse-Check');
} else {
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
} }
return { hoster, accountId, ...result };
} catch (err) {
return { hoster, accountId, status: 'error', message: err && err.message ? err.message : 'Health-Check fehlgeschlagen' };
}
}));
return { checkedAt: new Date().toISOString(), results }; if (hoster === 'vidmoly.me') {
const result = await withTimeout(
checkVidmolyHealth(hosterConfig),
HEALTH_CHECK_TIMEOUT,
'Vidmoly-Check'
);
return { hoster, ...result };
}
if (hoster === 'voe.sx') {
const result = await withTimeout(
checkVoeHealth(hosterConfig),
HEALTH_CHECK_TIMEOUT,
'VOE-Check'
);
return { hoster, ...result };
}
if (hoster === 'byse.sx') {
const result = await withTimeout(
checkByseHealth(hosterConfig),
HEALTH_CHECK_TIMEOUT,
'Byse-Check'
);
return { hoster, ...result };
}
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
} catch (err) {
return {
hoster,
status: 'error',
message: err && err.message ? err.message : 'Health-Check fehlgeschlagen'
};
}
});
const results = await Promise.all(checks);
return {
checkedAt: new Date().toISOString(),
results
};
} }
function createWindow() { function createWindow() {
@ -667,20 +680,6 @@ ipcMain.handle('start-upload', (_event, payload) => {
} }
}); });
uploadManager.on('account-failed', ({ hoster, accountId }) => {
const cfg = configStore.load();
const fallback = getNextFallbackAccount(cfg, hoster, accountId);
if (fallback) {
debugLog(`account-failed: ${hoster} ${accountId} → fallback to ${fallback.id}`);
uploadManager.switchAccount(hoster, fallback);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
}
} else {
debugLog(`account-failed: ${hoster} ${accountId} → no fallback available`);
}
});
uploadManager.on('batch-done', async (summary) => { uploadManager.on('batch-done', async (summary) => {
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`); debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
await configStore.appendHistory(summary); await configStore.appendHistory(summary);

View File

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

View File

@ -64,11 +64,6 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data)); ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
}, },
// Account switched event
onAccountSwitched: (callback) => {
ipcRenderer.on('account-switched', (_event, data) => callback(data));
},
// Drop Target // Drop Target
showDropTarget: () => ipcRenderer.invoke('show-drop-target'), showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'), hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
@ -104,6 +99,5 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.removeAllListeners('shutdown-countdown'); ipcRenderer.removeAllListeners('shutdown-countdown');
ipcRenderer.removeAllListeners('folder-monitor:new-files'); ipcRenderer.removeAllListeners('folder-monitor:new-files');
ipcRenderer.removeAllListeners('drop-target:files'); ipcRenderer.removeAllListeners('drop-target:files');
ipcRenderer.removeAllListeners('account-switched');
} }
}); });

View File

@ -1,15 +1,5 @@
const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx']; const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'];
// Dropdown options for "Add Account" modal: value -> label
const HOSTER_ADD_OPTIONS = [
{ value: 'doodstream.com', label: 'Doodstream (Web Login)', hoster: 'doodstream.com', authType: 'login' },
{ value: 'doodstream.com:api', label: 'Doodstream (API)', hoster: 'doodstream.com', authType: 'api' },
{ value: 'voe.sx', label: 'Voe (Web Login)', hoster: 'voe.sx', authType: 'login' },
{ value: 'voe.sx:api', label: 'Voe (API)', hoster: 'voe.sx', authType: 'api' },
{ value: 'vidmoly.me', label: 'Vidmoly (Web Login)', hoster: 'vidmoly.me', authType: 'login' },
{ value: 'byse.sx', label: 'Byse (API)', hoster: 'byse.sx', authType: 'api' }
];
// --- State --- // --- State ---
let selectedFiles = []; // { path, name, size } let selectedFiles = []; // { path, name, size }
let selectedUploadHosters = []; let selectedUploadHosters = [];
@ -17,8 +7,8 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
let hosterSettings = {}; let hosterSettings = {};
let uploading = false; let uploading = false;
let healthCheckRunning = false; let healthCheckRunning = false;
let accountStatuses = {}; // { accountId: { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } } let accountStatuses = {}; // { 'voe.sx': { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } }
let editingAccountId = null; // null = adding, string = editing account by ID let editingAccountHoster = null; // null = adding, string = editing
let autoHealthCheckEnabled = true; let autoHealthCheckEnabled = true;
let queuePersistTimer = null; let queuePersistTimer = null;
let settingsSaveTimer = null; let settingsSaveTimer = null;
@ -30,10 +20,6 @@ let queueJobs = []; // { id, file, fileName, hoster, status, bytesUploaded, byte
let _jobIndexById = new Map(); // id -> job (O(1) lookup) let _jobIndexById = new Map(); // id -> job (O(1) lookup)
let _jobIndexByUploadId = new Map(); // uploadId -> job let _jobIndexByUploadId = new Map(); // uploadId -> job
let selectedJobIds = new Set(); let selectedJobIds = new Set();
let _sessionTotalBytes = 0; // Total bytes ever added to queue this session
let _sessionUploadedBytes = 0; // Bytes fully uploaded this session (done jobs)
let _sessionTrackedJobs = new Set(); // Job IDs already counted for totalBytes
let _sessionDoneJobs = new Set(); // Job IDs already counted for uploadedBytes
let queueSortState = { key: 'filename', direction: 'asc' }; let queueSortState = { key: 'filename', direction: 'asc' };
// History state // History state
@ -118,11 +104,6 @@ async function init() {
} }
}); });
// Account switched notification
window.api.onAccountSwitched((data) => {
window.api.debugLog(`account-switched: ${data.hoster} ${data.fromAccountId} -> ${data.toAccountId}`);
});
// Drop target window: files dropped on the small floating window // Drop target window: files dropped on the small floating window
window.api.onDropTargetFiles((paths) => { window.api.onDropTargetFiles((paths) => {
addPathsToQueue(paths); addPathsToQueue(paths);
@ -151,26 +132,19 @@ document.querySelectorAll('.tab').forEach(tab => {
}); });
// --- Hoster selection --- // --- Hoster selection ---
function accountHasCreds(name, account) { function hosterHasCredentials(name, hoster) {
if (!account) return false; if (name === 'vidmoly.me') return !!(hoster.username && hoster.password);
if (account.authType === 'api') return !!account.apiKey; if (name === 'voe.sx' || name === 'doodstream.com') return !!(hoster.username && hoster.password) || !!hoster.apiKey;
if (account.authType === 'login') return !!(account.username && account.password); return !!hoster.apiKey;
// Fallback
if (name === 'vidmoly.me') return !!(account.username && account.password);
if (name === 'voe.sx' || name === 'doodstream.com') return !!(account.username && account.password) || !!account.apiKey;
return !!account.apiKey;
} }
// Returns hosters that have at least one enabled account with credentials
function getAvailableHosters() { function getAvailableHosters() {
const result = []; return HOSTERS
for (const name of HOSTERS) { .map(name => {
const accounts = config.hosters[name]; const hoster = config.hosters[name] || {};
if (!Array.isArray(accounts)) continue; return { name, hoster, hasCreds: hosterHasCredentials(name, hoster) };
const hasEnabledAccount = accounts.some(a => a.enabled !== false && accountHasCreds(name, a)); })
if (hasEnabledAccount) result.push({ name }); .filter(item => item.hasCreds && item.hoster.enabled !== false);
}
return result;
} }
function syncSelectedUploadHosters() { function syncSelectedUploadHosters() {
@ -178,8 +152,8 @@ function syncSelectedUploadHosters() {
selectedUploadHosters = selectedUploadHosters.filter(name => available.has(name)); selectedUploadHosters = selectedUploadHosters.filter(name => available.has(name));
if (selectedUploadHosters.length === 0) { if (selectedUploadHosters.length === 0) {
selectedUploadHosters = HOSTERS.filter(name => { selectedUploadHosters = HOSTERS.filter(name => {
const accounts = config.hosters[name]; const hoster = config.hosters[name] || {};
return Array.isArray(accounts) && accounts.some(a => a.enabled !== false && accountHasCreds(name, a)); return !!hoster.enabled && hosterHasCredentials(name, hoster);
}); });
} }
} }
@ -198,17 +172,24 @@ function getHosterLabel(name) {
return labels[name] || name; return labels[name] || name;
} }
function getAccountAuthLabel(account) { function getAccountModeParts(name, hoster) {
if (!account) return ''; if (!hoster) return [];
if (account.authType === 'api') return 'API'; const hasLogin = !!(hoster.username && hoster.password);
if (account.authType === 'login') return 'Web Login'; const hasApi = !!hoster.apiKey;
return '';
if (name === 'vidmoly.me') return hasLogin ? ['Login Web'] : [];
if (name === 'byse.sx') return hasApi ? ['API'] : [];
const parts = [];
if (hasLogin) parts.push('Login Web');
if (hasApi) parts.push('API');
return parts;
} }
function getAccountDisplayName(name, account) { function getAccountDisplayName(name, hoster) {
const authLabel = getAccountAuthLabel(account); const parts = getAccountModeParts(name, hoster);
return authLabel return parts.length > 0
? `${getHosterLabel(name)} (${authLabel})` ? `${getHosterLabel(name)} (${parts.join(' + ')})`
: getHosterLabel(name); : getHosterLabel(name);
} }
@ -221,43 +202,14 @@ function maskCredential(value, keep = 4) {
function ensureAccountStatusEntries() { function ensureAccountStatusEntries() {
const nextStatuses = {}; const nextStatuses = {};
for (const { account } of getAllAccountsFlat()) { for (const { name } of getAccountsWithCreds()) {
if (account.id) { nextStatuses[name] = accountStatuses[name] || { status: 'unchecked', message: '' };
nextStatuses[account.id] = accountStatuses[account.id] || { status: 'unchecked', message: '' };
}
} }
accountStatuses = nextStatuses; accountStatuses = nextStatuses;
} }
// Returns flat array of all accounts: [{ name, account, index }]
function getAllAccountsFlat() {
const result = [];
for (const name of HOSTERS) {
const accounts = config.hosters[name];
if (!Array.isArray(accounts)) continue;
accounts.forEach((account, index) => result.push({ name, account, index }));
}
return result;
}
// Returns flat array of accounts with credentials
function getAccountsWithCredsFlat() {
return getAllAccountsFlat().filter(({ name, account }) => accountHasCreds(name, account));
}
// Find account by ID across all hosters
function findAccountById(accountId) {
for (const name of HOSTERS) {
const accounts = config.hosters[name];
if (!Array.isArray(accounts)) continue;
const account = accounts.find(a => a.id === accountId);
if (account) return { name, account };
}
return null;
}
function scheduleStartupAccountCheck() { function scheduleStartupAccountCheck() {
const accounts = getAccountsWithCredsFlat(); const accounts = getAccountsWithCreds();
if (!accounts.length) return; if (!accounts.length) return;
setTimeout(() => { setTimeout(() => {
runHealthCheck('startup').catch(() => {}); runHealthCheck('startup').catch(() => {});
@ -271,7 +223,7 @@ function renderHosterSummary() {
if (hosters.length === 0) { if (hosters.length === 0) {
summary.textContent = 'Keine Upload-Ziele ausgewählt'; summary.textContent = 'Keine Upload-Ziele ausgewählt';
} else if (hosters.length === 1) { } else if (hosters.length === 1) {
summary.textContent = `Aktives Ziel: ${getHosterLabel(hosters[0])}`; summary.textContent = `Aktives Ziel: ${getAccountDisplayName(hosters[0], config.hosters[hosters[0]] || {})}`;
} else { } else {
summary.textContent = `${hosters.length} Ziele aktiv: ${hosters.map((name) => getHosterLabel(name)).join(', ')}`; summary.textContent = `${hosters.length} Ziele aktiv: ${hosters.map((name) => getHosterLabel(name)).join(', ')}`;
} }
@ -291,21 +243,17 @@ function renderHosterModal() {
list.innerHTML = available.map(item => { list.innerHTML = available.map(item => {
const checked = selectedUploadHosters.includes(item.name); const checked = selectedUploadHosters.includes(item.name);
// Get first enabled account's status for subtitle const h = config.hosters[item.name] || {};
const accounts = config.hosters[item.name] || []; const st = accountStatuses[item.name];
const enabledAccounts = accounts.filter(a => a.enabled !== false && accountHasCreds(item.name, a)); const subtitle = st && st.status === 'ok' ? 'Bereit'
const accountCount = enabledAccounts.length; : st && st.status === 'warn' ? 'Prüfung mit Warnung'
let subtitle = `${accountCount} Account${accountCount !== 1 ? 's' : ''}`; : st && st.status === 'error' ? 'Login-Fehler'
// Check if any account has ok status : `${getCredentialLabel(item.name, h)} hinterlegt`;
const hasOk = enabledAccounts.some(a => accountStatuses[a.id] && accountStatuses[a.id].status === 'ok');
const hasError = enabledAccounts.some(a => accountStatuses[a.id] && accountStatuses[a.id].status === 'error');
if (hasOk) subtitle += ' • Bereit';
else if (hasError) subtitle += ' • Fehler';
return ` return `
<label class="hoster-option${checked ? ' selected' : ''}" data-hoster-option="${item.name}"> <label class="hoster-option${checked ? ' selected' : ''}" data-hoster-option="${item.name}">
<input type="checkbox" data-hoster-modal="${item.name}" ${checked ? 'checked' : ''}> <input type="checkbox" data-hoster-modal="${item.name}" ${checked ? 'checked' : ''}>
<div class="hoster-option-main"> <div class="hoster-option-main">
<div class="hoster-option-title">${escapeHtml(getHosterLabel(item.name))}</div> <div class="hoster-option-title">${escapeHtml(getAccountDisplayName(item.name, h))}</div>
<div class="hoster-option-subtitle">${subtitle}</div> <div class="hoster-option-subtitle">${subtitle}</div>
</div> </div>
</label> </label>
@ -971,24 +919,6 @@ function showContextMenu(x, y) {
const startItem = menu.querySelector('[data-action="start-selected"]'); const startItem = menu.querySelector('[data-action="start-selected"]');
if (startItem) startItem.textContent = n > 1 ? `Ausgewählte starten (${n})` : 'Ausgewählte starten'; if (startItem) startItem.textContent = n > 1 ? `Ausgewählte starten (${n})` : 'Ausgewählte starten';
// Dynamic "cancel hoster" items
const cancelSep = menu.querySelector('.ctx-hoster-cancel-sep');
const cancelContainer = menu.querySelector('.ctx-hoster-cancel-items');
const activeHosters = [...new Set(queueJobs.filter(j => j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server' || j.status === 'preview').map(j => j.hoster))];
cancelContainer.innerHTML = '';
if (activeHosters.length > 0) {
cancelSep.style.display = '';
activeHosters.forEach(h => {
const item = document.createElement('div');
item.className = 'ctx-item ctx-item-danger';
item.dataset.action = `cancel-hoster:${h}`;
item.textContent = `${h} abbrechen`;
cancelContainer.appendChild(item);
});
} else {
cancelSep.style.display = 'none';
}
menu.style.display = 'block'; menu.style.display = 'block';
const menuX = Math.min(x, window.innerWidth - menu.offsetWidth - 5); const menuX = Math.min(x, window.innerWidth - menu.offsetWidth - 5);
menu.style.left = menuX + 'px'; menu.style.left = menuX + 'px';
@ -1186,24 +1116,6 @@ async function handleContextAction(action) {
} else if (action === 'always-on-top') { } else if (action === 'always-on-top') {
alwaysOnTopState = !alwaysOnTopState; alwaysOnTopState = !alwaysOnTopState;
await window.api.setAlwaysOnTop(alwaysOnTopState); await window.api.setAlwaysOnTop(alwaysOnTopState);
} else if (action.startsWith('cancel-hoster:')) {
const hoster = action.replace('cancel-hoster:', '');
const jobIds = [];
for (const job of queueJobs) {
if (job.hoster === hoster && (job.status === 'uploading' || job.status === 'queued' || job.status === 'retrying' || job.status === 'getting-server' || job.status === 'preview')) {
jobIds.push(job.id);
// Mark queued/preview jobs as error immediately
if (job.status === 'queued' || job.status === 'preview') {
job.status = 'error';
job.error = 'Hoster abgebrochen';
}
}
}
// Cancel active uploads via IPC
if (jobIds.length > 0) await window.api.cancelSelectedJobs(jobIds);
renderQueueTable();
updateStatusBar();
updateQueueActionButtons();
} else if (action.startsWith('shutdown-')) { } else if (action.startsWith('shutdown-')) {
const mode = action.replace('shutdown-', ''); const mode = action.replace('shutdown-', '');
await window.api.setShutdownAfterFinish(mode); await window.api.setShutdownAfterFinish(mode);
@ -1390,11 +1302,6 @@ function handleProgress(data) {
job.status = data.status; job.status = data.status;
job.bytesUploaded = data.bytesUploaded || 0; job.bytesUploaded = data.bytesUploaded || 0;
job.bytesTotal = data.bytesTotal || job.bytesTotal; job.bytesTotal = data.bytesTotal || job.bytesTotal;
// Track session total bytes (survives removeFromQueueOnDone)
if (job.bytesTotal > 0 && !_sessionTrackedJobs.has(job.id)) {
_sessionTotalBytes += job.bytesTotal;
_sessionTrackedJobs.add(job.id);
}
job.speedKbs = data.speedKbs || 0; job.speedKbs = data.speedKbs || 0;
job.elapsed = data.elapsed || 0; job.elapsed = data.elapsed || 0;
job.remaining = data.remaining || 0; job.remaining = data.remaining || 0;
@ -1410,12 +1317,6 @@ function handleProgress(data) {
maybeAddSessionFile(job); maybeAddSessionFile(job);
// Track session uploaded bytes (survives removeFromQueueOnDone)
if (job.status === 'done' && !_sessionDoneJobs.has(job.id)) {
_sessionUploadedBytes += job.bytesTotal || 0;
_sessionDoneJobs.add(job.id);
}
// Remove finished jobs from queue immediately if setting is enabled // Remove finished jobs from queue immediately if setting is enabled
if (job.status === 'done' && config.globalSettings && config.globalSettings.removeFromQueueOnDone) { if (job.status === 'done' && config.globalSettings && config.globalSettings.removeFromQueueOnDone) {
removeJobFromIndex(job); removeJobFromIndex(job);
@ -1507,7 +1408,7 @@ async function retrySelectedJobs() {
const retryJobs = []; const retryJobs = [];
queueJobs.forEach(j => { queueJobs.forEach(j => {
if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) { if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) {
j.status = uploading ? 'queued' : 'preview'; j.status = 'preview';
j.error = null; j.error = null;
j.result = null; j.result = null;
j.bytesUploaded = 0; j.bytesUploaded = 0;
@ -1640,6 +1541,21 @@ function maybeAddSessionFile(job) {
} }
} }
if (job.status === 'error') {
const errorText = `[Fehler] ${job.error || ''}`;
if (!sessionFilesData.some((row) => row.isError && row.filename === job.fileName && row.host === job.hoster && row.link === errorText)) {
sessionFilesData.push({
date: dt.text,
dateTs: dt.ts,
filename: job.fileName || '',
host: job.hoster || '',
link: errorText,
isError: true,
order: sessionFilesData.length
});
renderRecentUploadsPanel();
}
}
} }
function applySummaryResults(summary) { function applySummaryResults(summary) {
@ -1721,26 +1637,15 @@ function updateStatusBar() {
document.getElementById('sbState').textContent = stateText; document.getElementById('sbState').textContent = stateText;
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0); document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
// Session-based bytes: survive removeFromQueueOnDone const uploadedSize = Math.max(0, stats.totalSize - stats.remainingSize);
// Uploaded = done jobs (session) + in-progress bytes still in queue document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(stats.totalSize)}`;
let inProgressBytes = 0;
for (const job of queueJobs) {
if (job.status === 'uploading' || job.status === 'getting-server' || job.status === 'retrying') {
inProgressBytes += job.bytesUploaded || 0;
}
}
const uploadedSize = _sessionUploadedBytes + inProgressBytes;
const totalSize = Math.max(stats.totalSize, _sessionTotalBytes);
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`; document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`; document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`;
document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`; document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`;
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`; document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`;
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`; document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`;
const sessionDone = sessionFilesData.filter(r => !r.isError).length; document.getElementById('sbDoneCount').textContent = `Done: ${stats.done}`;
const sessionErrors = sessionFilesData.filter(r => r.isError).length; document.getElementById('sbErrorCount').textContent = `Error: ${stats.errors}`;
document.getElementById('sbDoneCount').textContent = `Done: ${sessionDone}`;
document.getElementById('sbErrorCount').textContent = `Error: ${sessionErrors}`;
} }
// --- Health Check --- // --- Health Check ---
@ -1768,14 +1673,11 @@ async function executeHealthCheck(hosters, mode) {
const result = await window.api.runHealthCheck({ hosters }); const result = await window.api.runHealthCheck({ hosters });
const rows = result && Array.isArray(result.results) ? result.results : []; const rows = result && Array.isArray(result.results) ? result.results : [];
rows.forEach((row) => { rows.forEach((row) => {
if (!row) return; if (!row || !row.hoster) return;
const key = row.accountId || row.hoster; accountStatuses[row.hoster] = {
if (key) { status: row.status || 'unchecked',
accountStatuses[key] = { message: row.message || ''
status: row.status || 'unchecked', };
message: row.message || ''
};
}
}); });
renderHealthCheckResults(rows); renderHealthCheckResults(rows);
renderAccounts(); renderAccounts();
@ -1785,25 +1687,17 @@ async function executeHealthCheck(hosters, mode) {
async function runHealthCheck(mode = 'manual', requestedHosters = null) { async function runHealthCheck(mode = 'manual', requestedHosters = null) {
if (healthCheckRunning || (uploading && mode === 'manual')) return []; if (healthCheckRunning || (uploading && mode === 'manual')) return [];
// Build check list: all enabled accounts with creds const hosters = Array.isArray(requestedHosters) && requestedHosters.length > 0
let hosters; ? requestedHosters
if (Array.isArray(requestedHosters) && requestedHosters.length > 0) { : HOSTERS.filter((name) => hosterHasCredentials(name, config.hosters[name] || {}));
hosters = requestedHosters;
} else {
hosters = getAccountsWithCredsFlat()
.filter(({ account }) => account.enabled !== false)
.map(({ name, account }) => ({ hoster: name, accountId: account.id }));
}
if (hosters.length === 0) { if (hosters.length === 0) {
if (mode === 'manual') alert('Keine Hoster mit Zugangsdaten für einen Check.'); if (mode === 'manual') alert('Keine Hoster mit Zugangsdaten für einen Check.');
return []; return [];
} }
healthCheckRunning = true; healthCheckRunning = true;
// Mark all accounts as checking hosters.forEach((hoster) => {
for (const h of hosters) { accountStatuses[hoster] = { status: 'checking', message: '' };
const key = typeof h === 'string' ? h : (h.accountId || h.hoster); });
accountStatuses[key] = { status: 'checking', message: '' };
}
renderAccounts(); renderAccounts();
try { try {
return await executeHealthCheck(hosters, mode); return await executeHealthCheck(hosters, mode);
@ -1822,7 +1716,7 @@ function renderSettings() {
container.innerHTML = ''; container.innerHTML = '';
const globalSettings = config.globalSettings || {}; const globalSettings = config.globalSettings || {};
const configuredAccounts = getAvailableHosters(); const configuredAccounts = getAccountsWithCreds();
const generalPanel = document.createElement('div'); const generalPanel = document.createElement('div');
generalPanel.className = 'hoster-settings-panel'; generalPanel.className = 'hoster-settings-panel';
generalPanel.innerHTML = ` generalPanel.innerHTML = `
@ -2028,7 +1922,7 @@ function renderSettings() {
container.appendChild(empty); container.appendChild(empty);
} }
for (const { name } of configuredAccounts) { for (const { name, hoster } of configuredAccounts) {
const hs = hosterSettings[name] || {}; const hs = hosterSettings[name] || {};
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? (hs.maxSpeedKbs / 1024).toFixed(2).replace(/\.00$/, '') : '0'; const maxSpeedMbs = hs.maxSpeedKbs > 0 ? (hs.maxSpeedKbs / 1024).toFixed(2).replace(/\.00$/, '') : '0';
@ -2038,7 +1932,7 @@ function renderSettings() {
panel.innerHTML = ` panel.innerHTML = `
<div class="hoster-panel-header" data-hoster="${name}"> <div class="hoster-panel-header" data-hoster="${name}">
<span class="panel-arrow">&#9654;</span> <span class="panel-arrow">&#9654;</span>
<span class="panel-title">${escapeHtml(getHosterLabel(name))}</span> <span class="panel-title">${escapeHtml(getAccountDisplayName(name, hoster))}</span>
<span class="panel-status active">Aktiv</span> <span class="panel-status active">Aktiv</span>
</div> </div>
<div class="hoster-panel-body" data-panel="${name}" style="display:none"> <div class="hoster-panel-body" data-panel="${name}" style="display:none">
@ -2214,14 +2108,25 @@ async function saveSettings(options = {}) {
} }
// --- Accounts --- // --- Accounts ---
function getCredentialLabel(name, account) { function getAccountsWithCreds() {
if (!account) return 'Keine Zugangsdaten'; return HOSTERS
if (account.authType === 'api') return `API: ${maskCredential(account.apiKey) || 'nicht gesetzt'}`; .map(name => ({ name, hoster: config.hosters[name] || {} }))
if (account.authType === 'login') return `Login: ${account.username || 'nicht gesetzt'}`; .filter(item => hosterHasCredentials(item.name, item.hoster));
// Fallback }
if (account.username && account.password) return `Login: ${account.username}`;
if (account.apiKey) return `API: ${maskCredential(account.apiKey)}`; function getHostersWithoutCreds() {
return 'Keine Zugangsdaten'; return HOSTERS.filter(name => !hosterHasCredentials(name, config.hosters[name] || {}));
}
function getCredentialLabel(name, hoster) {
if (name === 'vidmoly.me') return `Login: ${hoster.username || 'nicht gesetzt'}`;
if (name === 'voe.sx' || name === 'doodstream.com') {
const parts = [];
if (hoster.username && hoster.password) parts.push(`Login: ${hoster.username}`);
if (hoster.apiKey) parts.push(`API: ${maskCredential(hoster.apiKey)}`);
return parts.join(' • ') || 'Keine Zugangsdaten';
}
return `API: ${maskCredential(hoster.apiKey) || 'nicht gesetzt'}`;
} }
function renderAccounts() { function renderAccounts() {
@ -2229,11 +2134,11 @@ function renderAccounts() {
if (!container) return; if (!container) return;
ensureAccountStatusEntries(); ensureAccountStatusEntries();
const allAccounts = getAllAccountsFlat(); const accounts = getAccountsWithCreds();
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn'); const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning; if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
if (allAccounts.length === 0) { if (accounts.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="accounts-empty"> <div class="accounts-empty">
<p>Keine Accounts vorhanden</p> <p>Keine Accounts vorhanden</p>
@ -2242,51 +2147,33 @@ function renderAccounts() {
return; return;
} }
// Group by hoster for drag reorder sections container.innerHTML = accounts.map(({ name, hoster }) => {
const byHoster = {}; const isDisabled = hoster.enabled === false;
for (const { name, account } of allAccounts) { const st = accountStatuses[name] || { status: 'unchecked', message: '' };
if (!byHoster[name]) byHoster[name] = []; const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
byHoster[name].push(account); const statusLabel = isDisabled ? 'Deaktiviert' : (statusLabels[st.status] || 'Nicht geprüft');
} const statusClass = isDisabled ? 'disabled' : st.status;
const credLabel = getCredentialLabel(name, hoster);
const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
let html = ''; return `
for (const name of HOSTERS) { <div class="account-card${isDisabled ? ' account-disabled' : ''}" data-account="${name}">
const accounts = byHoster[name]; <div class="account-card-info">
if (!accounts || accounts.length === 0) continue; <div class="account-card-title">${escapeHtml(getAccountDisplayName(name, hoster))}</div>
html += `<div class="account-hoster-group" data-hoster-group="${name}"> <div class="account-card-subtitle" title="${escapeAttr(credLabel)}">${escapeHtml(credLabel)}${st.message && !isDisabled ? `${escapeHtml(st.message)}` : ''}</div>
<div class="account-hoster-group-title">${escapeHtml(getHosterLabel(name))}</div>`; </div>
accounts.forEach((account, idx) => { <span class="account-status status-${statusClass}">
const isDisabled = account.enabled === false; <span class="account-status-dot"></span>
const st = accountStatuses[account.id] || { status: 'unchecked', message: '' }; ${statusLabel}
const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' }; </span>
const statusLabel = isDisabled ? 'Deaktiviert' : (statusLabels[st.status] || 'Nicht geprüft'); <div class="account-card-actions">
const statusClass = isDisabled ? 'disabled' : st.status; <button class="btn btn-xs btn-secondary" data-account-toggle="${name}">${toggleLabel}</button>
const credLabel = getCredentialLabel(name, account); <button class="btn btn-xs btn-secondary" data-account-check="${name}" ${isDisabled ? 'disabled' : ''}>Prüfen</button>
const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren'; <button class="btn btn-xs btn-secondary" data-account-edit="${name}">Bearbeiten</button>
const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`; <button class="btn btn-xs btn-danger" data-account-delete="${name}">Löschen</button>
</div>
html += ` </div>`;
<div class="account-card${isDisabled ? ' account-disabled' : ''}" data-account-id="${account.id}" data-account-hoster="${name}" draggable="true"> }).join('');
<div class="account-card-drag-handle" title="Ziehen zum Sortieren">&#9776;</div>
<div class="account-card-info">
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, account))} <span class="account-priority-badge">${priorityLabel}</span></div>
<div class="account-card-subtitle" title="${escapeAttr(credLabel)}">${escapeHtml(credLabel)}${st.message && !isDisabled ? `${escapeHtml(st.message)}` : ''}</div>
</div>
<span class="account-status status-${statusClass}">
<span class="account-status-dot"></span>
${statusLabel}
</span>
<div class="account-card-actions">
<button class="btn btn-xs btn-secondary" data-account-toggle="${account.id}">${toggleLabel}</button>
<button class="btn btn-xs btn-secondary" data-account-check="${account.id}" ${isDisabled ? 'disabled' : ''}>Prüfen</button>
<button class="btn btn-xs btn-secondary" data-account-edit="${account.id}">Bearbeiten</button>
<button class="btn btn-xs btn-danger" data-account-delete="${account.id}">Löschen</button>
</div>
</div>`;
});
html += '</div>';
}
container.innerHTML = html;
// Wire up buttons // Wire up buttons
container.querySelectorAll('[data-account-toggle]').forEach(btn => { container.querySelectorAll('[data-account-toggle]').forEach(btn => {
@ -2301,73 +2188,14 @@ function renderAccounts() {
container.querySelectorAll('[data-account-check]').forEach(btn => { container.querySelectorAll('[data-account-check]').forEach(btn => {
btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck)); btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
}); });
// Drag-and-drop reorder within each hoster group
setupAccountDragReorder(container);
} }
function setupAccountDragReorder(container) { async function toggleAccount(hosterName) {
let draggedCard = null; const hosters = { ...config.hosters };
container.querySelectorAll('.account-card[draggable]').forEach(card => { const hoster = { ...hosters[hosterName] };
card.addEventListener('dragstart', (e) => { hoster.enabled = hoster.enabled === false ? true : false;
draggedCard = card; hosters[hosterName] = hoster;
card.classList.add('dragging'); await window.api.saveConfig({ hosters });
e.dataTransfer.effectAllowed = 'move';
});
card.addEventListener('dragend', () => {
if (draggedCard) draggedCard.classList.remove('dragging');
draggedCard = null;
container.querySelectorAll('.account-card').forEach(c => c.classList.remove('drag-over-above', 'drag-over-below'));
});
card.addEventListener('dragover', (e) => {
e.preventDefault();
if (!draggedCard || draggedCard === card) return;
if (draggedCard.dataset.accountHoster !== card.dataset.accountHoster) return;
e.dataTransfer.dropEffect = 'move';
const rect = card.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
card.classList.toggle('drag-over-above', e.clientY < midY);
card.classList.toggle('drag-over-below', e.clientY >= midY);
});
card.addEventListener('dragleave', () => {
card.classList.remove('drag-over-above', 'drag-over-below');
});
card.addEventListener('drop', async (e) => {
e.preventDefault();
card.classList.remove('drag-over-above', 'drag-over-below');
if (!draggedCard || draggedCard === card) return;
const hosterName = card.dataset.accountHoster;
if (draggedCard.dataset.accountHoster !== hosterName) return;
const draggedId = draggedCard.dataset.accountId;
const targetId = card.dataset.accountId;
const accounts = config.hosters[hosterName];
if (!Array.isArray(accounts)) return;
const fromIdx = accounts.findIndex(a => a.id === draggedId);
const toIdx = accounts.findIndex(a => a.id === targetId);
if (fromIdx < 0 || toIdx < 0) return;
// Move account in array
const [moved] = accounts.splice(fromIdx, 1);
const rect = card.getBoundingClientRect();
const insertBefore = e.clientY < rect.top + rect.height / 2;
const newToIdx = accounts.findIndex(a => a.id === targetId);
accounts.splice(insertBefore ? newToIdx : newToIdx + 1, 0, moved);
// Save and re-render
await window.api.saveConfig({ hosters: config.hosters });
config = await window.api.getConfig();
renderAccounts();
});
});
}
async function toggleAccount(accountId) {
const found = findAccountById(accountId);
if (!found) return;
found.account.enabled = found.account.enabled === false ? true : false;
await window.api.saveConfig({ hosters: config.hosters });
config = await window.api.getConfig(); config = await window.api.getConfig();
syncSelectedUploadHosters(); syncSelectedUploadHosters();
renderAccounts(); renderAccounts();
@ -2376,51 +2204,66 @@ async function toggleAccount(accountId) {
renderSettings(); renderSettings();
} }
async function checkSingleAccount(accountId) { async function checkSingleAccount(hosterName) {
if (!accountId || healthCheckRunning) return; if (!hosterName || healthCheckRunning) return;
const found = findAccountById(accountId);
if (!found) return;
healthCheckRunning = true; healthCheckRunning = true;
accountStatuses[accountId] = { status: 'checking', message: '' }; accountStatuses[hosterName] = { status: 'checking', message: '' };
renderAccounts(); renderAccounts();
try { try {
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] }); const rows = await executeHealthCheck([hosterName], 'manual');
const rows = result && Array.isArray(result.results) ? result.results : []; const row = rows.find(r => r.hoster === hosterName);
const row = rows.find(r => r.accountId === accountId); if (row) accountStatuses[hosterName] = { status: row.status || 'error', message: row.message || '' };
if (row) accountStatuses[accountId] = { status: row.status || 'error', message: row.message || '' };
} catch (err) { } catch (err) {
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' }; accountStatuses[hosterName] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
} finally { } finally {
healthCheckRunning = false; healthCheckRunning = false;
} }
renderAccounts(); renderAccounts();
} }
function getCredsFieldsHtml(authType, account) { function getCredsFieldsHtml(name, hoster) {
account = account || {}; hoster = hoster || {};
if (authType === 'login') { if (name === 'vidmoly.me') {
return ` return `
<div class="settings-row"> <div class="settings-row">
<label>Username / E-Mail</label> <label>Username</label>
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(account.username || '')}" placeholder="Username oder E-Mail"> <input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="Username">
</div> </div>
<div class="settings-row"> <div class="settings-row">
<label>Passwort</label> <label>Passwort</label>
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(account.password || '')}" placeholder="Passwort"> <input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button> <button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>`; </div>`;
} }
// API key if (name === 'voe.sx' || name === 'doodstream.com') {
return `
<div class="settings-row">
<label>E-Mail (Login)</label>
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="E-Mail für Login">
</div>
<div class="settings-row">
<label>Passwort (Login)</label>
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort für Login">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>
<div class="settings-row">
<label>API Key (optional)</label>
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key (Fallback)">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>
<p class="hint" style="margin:4px 0 0;opacity:0.6">Login wird bevorzugt. API-Key nur als Fallback.</p>`;
}
// Default: API key only
return ` return `
<div class="settings-row"> <div class="settings-row">
<label>API Key</label> <label>API Key</label>
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(account.apiKey || '')}" placeholder="API Key"> <input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button> <button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>`; </div>`;
} }
function openAccountModal(editAccountId) { function openAccountModal(editHoster) {
editingAccountId = editAccountId || null; editingAccountHoster = editHoster || null;
const modal = document.getElementById('accountModal'); const modal = document.getElementById('accountModal');
const title = document.getElementById('accountModalTitle'); const title = document.getElementById('accountModalTitle');
const subtitle = document.getElementById('accountModalSubtitle'); const subtitle = document.getElementById('accountModalSubtitle');
@ -2433,26 +2276,28 @@ function openAccountModal(editAccountId) {
statusEl.textContent = ''; statusEl.textContent = '';
statusEl.className = 'account-modal-status'; statusEl.className = 'account-modal-status';
if (editingAccountId) { if (editingAccountHoster) {
// Edit mode // Edit mode
const found = findAccountById(editingAccountId);
if (!found) return;
title.textContent = 'Account bearbeiten'; title.textContent = 'Account bearbeiten';
subtitle.textContent = `Zugangsdaten für ${getAccountDisplayName(found.name, found.account)} bearbeiten.`; subtitle.textContent = `Zugangsdaten für ${getAccountDisplayName(editingAccountHoster, config.hosters[editingAccountHoster] || {})} bearbeiten.`;
hosterRow.style.display = 'none'; hosterRow.style.display = 'none';
saveBtn.textContent = 'Speichern & prüfen'; saveBtn.textContent = 'Speichern & prüfen';
credsContainer.innerHTML = getCredsFieldsHtml(found.account.authType || 'login', found.account); const hoster = config.hosters[editingAccountHoster] || {};
credsContainer.innerHTML = getCredsFieldsHtml(editingAccountHoster, hoster);
} else { } else {
// Add mode — always show all options (multiple accounts per hoster allowed) // Add mode
title.textContent = 'Account hinzufügen'; title.textContent = 'Account hinzufügen';
subtitle.textContent = 'Wähle einen Hoster und gib deine Zugangsdaten ein.'; subtitle.textContent = 'Wähle einen Hoster und gib deine Zugangsdaten ein.';
hosterRow.style.display = 'flex'; hosterRow.style.display = 'flex';
saveBtn.textContent = 'Anlegen & prüfen'; saveBtn.textContent = 'Anlegen & prüfen';
hosterSelect.innerHTML = HOSTER_ADD_OPTIONS.map(opt => const available = getHostersWithoutCreds();
`<option value="${opt.value}">${escapeHtml(opt.label)}</option>` if (available.length === 0) {
).join(''); hosterSelect.innerHTML = '<option value="">Alle Hoster bereits eingerichtet</option>';
const firstOpt = HOSTER_ADD_OPTIONS[0]; credsContainer.innerHTML = '';
credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {}); } else {
hosterSelect.innerHTML = available.map(name => `<option value="${name}">${getHosterLabel(name)}</option>`).join('');
credsContainer.innerHTML = getCredsFieldsHtml(available[0], {});
}
} }
// Toggle visibility buttons // Toggle visibility buttons
@ -2468,16 +2313,14 @@ function openAccountModal(editAccountId) {
function closeAccountModal() { function closeAccountModal() {
document.getElementById('accountModal').style.display = 'none'; document.getElementById('accountModal').style.display = 'none';
editingAccountId = null; editingAccountHoster = null;
} }
function openDeleteAccountModal(accountId) { function openDeleteAccountModal(hosterName) {
const found = findAccountById(accountId);
if (!found) return;
const modal = document.getElementById('deleteAccountModal'); const modal = document.getElementById('deleteAccountModal');
const msg = document.getElementById('deleteAccountMessage'); const msg = document.getElementById('deleteAccountMessage');
msg.textContent = `Account "${getAccountDisplayName(found.name, found.account)}" wirklich löschen?`; msg.textContent = `Account für "${getHosterLabel(hosterName)}" wirklich löschen? Alle Zugangsdaten werden entfernt.`;
modal.dataset.accountId = accountId; modal.dataset.hoster = hosterName;
modal.style.display = 'flex'; modal.style.display = 'flex';
} }
@ -2485,20 +2328,22 @@ function closeDeleteModal() {
document.getElementById('deleteAccountModal').style.display = 'none'; document.getElementById('deleteAccountModal').style.display = 'none';
} }
async function deleteAccount(accountId) { async function deleteAccount(hosterName) {
const found = findAccountById(accountId); const hosters = { ...config.hosters };
if (!found) return; // Reset credentials to defaults
// Remove account from the array if (hosterName === 'vidmoly.me') {
const accounts = config.hosters[found.name]; hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
if (Array.isArray(accounts)) { } else if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
config.hosters[found.name] = accounts.filter(a => a.id !== accountId); hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
} else {
hosters[hosterName] = { enabled: false, apiKey: '' };
} }
delete accountStatuses[accountId]; delete accountStatuses[hosterName];
await window.api.saveConfig({ hosters: config.hosters }); await window.api.saveConfig({ hosters });
config = await window.api.getConfig(); config = await window.api.getConfig();
ensureAccountStatusEntries(); ensureAccountStatusEntries();
syncSelectedUploadHosters(); syncSelectedUploadHosters();
if (getAllAccountsFlat().length === 0) renderHealthCheckResults([]); if (getAccountsWithCreds().length === 0) renderHealthCheckResults([]);
renderAccounts(); renderAccounts();
renderHosterSummary(); renderHosterSummary();
renderHosterModal(); renderHosterModal();
@ -2506,39 +2351,27 @@ async function deleteAccount(accountId) {
closeDeleteModal(); closeDeleteModal();
} }
function readAccountCredsFromModal(authType) { function readAccountCredsFromModal(hosterName) {
if (authType === 'login') { if (hosterName === 'vidmoly.me') {
const username = (document.getElementById('accField_username')?.value || '').trim(); const username = (document.getElementById('accField_username')?.value || '').trim();
const password = (document.getElementById('accField_password')?.value || '').trim(); const password = (document.getElementById('accField_password')?.value || '').trim();
return { enabled: !!(username && password), authType: 'login', username, password }; return { enabled: !!(username && password), authType: 'login', username, password };
} }
// API if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
const username = (document.getElementById('accField_username')?.value || '').trim();
const password = (document.getElementById('accField_password')?.value || '').trim();
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
return { enabled: !!(username && password) || !!apiKey, username, password, apiKey };
}
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim(); const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
return { enabled: !!apiKey, authType: 'api', apiKey }; return { enabled: !!apiKey, apiKey };
} }
async function saveAccount() { async function saveAccount() {
let hosterName, authType, accountId; const hosterName = editingAccountHoster || document.getElementById('accountHosterSelect')?.value;
if (!hosterName) return;
if (editingAccountId) { const creds = readAccountCredsFromModal(hosterName);
// Edit existing account
const found = findAccountById(editingAccountId);
if (!found) return;
hosterName = found.name;
authType = found.account.authType || 'login';
accountId = editingAccountId;
} else {
// Add new account
const selectValue = document.getElementById('accountHosterSelect')?.value;
if (!selectValue) return;
const opt = HOSTER_ADD_OPTIONS.find(o => o.value === selectValue);
if (!opt) return;
hosterName = opt.hoster;
authType = opt.authType;
accountId = `${hosterName}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
}
const creds = readAccountCredsFromModal(authType);
if (!creds.enabled) { if (!creds.enabled) {
const statusEl = document.getElementById('accountModalStatus'); const statusEl = document.getElementById('accountModalStatus');
statusEl.textContent = 'Bitte Zugangsdaten eingeben.'; statusEl.textContent = 'Bitte Zugangsdaten eingeben.';
@ -2547,18 +2380,9 @@ async function saveAccount() {
} }
// Save credentials // Save credentials
if (!Array.isArray(config.hosters[hosterName])) config.hosters[hosterName] = []; const hosters = { ...config.hosters };
if (editingAccountId) { hosters[hosterName] = creds;
// Update existing account in array await window.api.saveConfig({ hosters });
const idx = config.hosters[hosterName].findIndex(a => a.id === editingAccountId);
if (idx >= 0) {
config.hosters[hosterName][idx] = { ...config.hosters[hosterName][idx], ...creds };
}
} else {
// Add new account
config.hosters[hosterName].push({ id: accountId, ...creds });
}
await window.api.saveConfig({ hosters: config.hosters });
config = await window.api.getConfig(); config = await window.api.getConfig();
// Show checking status // Show checking status
@ -2568,31 +2392,30 @@ async function saveAccount() {
statusEl.className = 'account-modal-status checking'; statusEl.className = 'account-modal-status checking';
saveBtn.disabled = true; saveBtn.disabled = true;
accountStatuses[accountId] = { status: 'checking', message: '' }; accountStatuses[hosterName] = { status: 'checking', message: '' };
syncSelectedUploadHosters(); syncSelectedUploadHosters();
renderAccounts(); renderAccounts();
renderHosterSummary(); renderHosterSummary();
renderHosterModal(); renderHosterModal();
renderSettings(); renderSettings();
// Run health check for this specific account // Run health check
try { try {
const result = await window.api.runHealthCheck({ hosters: [{ hoster: hosterName, accountId }] }); const rows = await executeHealthCheck([hosterName], 'auto');
const rows = result && Array.isArray(result.results) ? result.results : []; const row = rows.find(r => r.hoster === hosterName);
const row = rows.find(r => r.accountId === accountId);
if (row && (row.status === 'ok' || row.status === 'warn')) { if (row && (row.status === 'ok' || row.status === 'warn')) {
accountStatuses[accountId] = { status: row.status || 'ok', message: row.message || '' }; accountStatuses[hosterName] = { status: row.status || 'ok', message: row.message || '' };
statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!'; statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!';
statusEl.className = 'account-modal-status ok'; statusEl.className = 'account-modal-status ok';
setTimeout(() => closeAccountModal(), 1200); setTimeout(() => closeAccountModal(), 1200);
} else { } else {
const msg = (row && row.message) || 'Login fehlgeschlagen'; const msg = (row && row.message) || 'Login fehlgeschlagen';
accountStatuses[accountId] = { status: 'error', message: msg }; accountStatuses[hosterName] = { status: 'error', message: msg };
statusEl.textContent = msg; statusEl.textContent = msg;
statusEl.className = 'account-modal-status error'; statusEl.className = 'account-modal-status error';
} }
} catch (err) { } catch (err) {
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' }; accountStatuses[hosterName] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
statusEl.textContent = err.message || 'Prüfung fehlgeschlagen'; statusEl.textContent = err.message || 'Prüfung fehlgeschlagen';
statusEl.className = 'account-modal-status error'; statusEl.className = 'account-modal-status error';
} finally { } finally {
@ -2952,10 +2775,8 @@ function setupListeners() {
// Account hoster select change → update credential fields // Account hoster select change → update credential fields
document.getElementById('accountHosterSelect').addEventListener('change', (e) => { document.getElementById('accountHosterSelect').addEventListener('change', (e) => {
const opt = HOSTER_ADD_OPTIONS.find(o => o.value === e.target.value);
const authType = opt ? opt.authType : 'login';
const credsContainer = document.getElementById('accountCredsFields'); const credsContainer = document.getElementById('accountCredsFields');
credsContainer.innerHTML = getCredsFieldsHtml(authType, {}); credsContainer.innerHTML = getCredsFieldsHtml(e.target.value, {});
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => { credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const input = btn.previousElementSibling; const input = btn.previousElementSibling;
@ -2971,8 +2792,8 @@ function setupListeners() {
document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal); document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
document.getElementById('confirmDeleteBtn').addEventListener('click', () => { document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
const modal = document.getElementById('deleteAccountModal'); const modal = document.getElementById('deleteAccountModal');
const accountId = modal.dataset.accountId; const hoster = modal.dataset.hoster;
if (accountId) deleteAccount(accountId); if (hoster) deleteAccount(hoster);
}); });
document.getElementById('deleteAccountModal').addEventListener('click', (e) => { document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
if (e.target.id === 'deleteAccountModal') closeDeleteModal(); if (e.target.id === 'deleteAccountModal') closeDeleteModal();

View File

@ -265,8 +265,6 @@
<div class="ctx-separator"></div> <div class="ctx-separator"></div>
<div class="ctx-item" data-action="delete-selected">Entfernen</div> <div class="ctx-item" data-action="delete-selected">Entfernen</div>
<div class="ctx-item" data-action="delete-all">Alle entfernen</div> <div class="ctx-item" data-action="delete-all">Alle entfernen</div>
<div class="ctx-separator ctx-hoster-cancel-sep" style="display:none"></div>
<div class="ctx-hoster-cancel-items"></div>
</div> </div>
<div class="context-menu" id="recentContextMenu" style="display:none"> <div class="context-menu" id="recentContextMenu" style="display:none">

View File

@ -605,8 +605,6 @@ body {
position: relative; position: relative;
} }
.ctx-item:hover { background: rgba(102, 126, 234, 0.2); } .ctx-item:hover { background: rgba(102, 126, 234, 0.2); }
.ctx-item-danger { color: var(--danger); }
.ctx-item-danger:hover { background: rgba(231, 76, 60, 0.2); }
.ctx-separator { height: 1px; margin: 4px 8px; background: var(--border); } .ctx-separator { height: 1px; margin: 4px 8px; background: var(--border); }
.ctx-submenu { position: relative; } .ctx-submenu { position: relative; }
.ctx-submenu-items { .ctx-submenu-items {
@ -812,44 +810,6 @@ body {
.account-modal-status.ok { color: var(--success); } .account-modal-status.ok { color: var(--success); }
.account-modal-status.error { color: var(--danger); } .account-modal-status.error { color: var(--danger); }
/* Multi-account: drag handle, priority badge, hoster group */
.account-card-drag-handle {
cursor: grab;
font-size: 14px;
color: var(--text-dim);
padding: 2px 4px;
flex-shrink: 0;
user-select: none;
}
.account-card-drag-handle:hover { color: var(--text-muted); }
.account-card.dragging { opacity: 0.4; }
.account-card.drag-over-above { border-top: 2px solid var(--accent); }
.account-card.drag-over-below { border-bottom: 2px solid var(--accent); }
.account-priority-badge {
font-size: 10px;
font-weight: 500;
color: var(--text-dim);
background: rgba(255, 255, 255, 0.06);
padding: 1px 6px;
border-radius: 3px;
margin-left: 6px;
}
.account-hoster-group {
margin-bottom: 12px;
}
.account-hoster-group-title {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
padding-left: 4px;
}
.account-hoster-group .account-card { margin-bottom: 4px; }
.accounts-empty { .accounts-empty {
text-align: center; text-align: center;
padding: 48px 16px; padding: 48px 16px;