feat: multi-account support with primary/fallback and separate API/login types
- Multiple accounts per hoster with drag-sortable priority (primary + fallbacks) - Separate account types: Web Login and API selectable per hoster - Account fallback: after all retries fail, automatically switches to next fallback account - Fix: Byse health check returning [Fehler] OK when API responds with msg "OK" - Fix: retry during active upload sets status to "Wartet" instead of "Bereit" - Config migration from single-object to multi-account array format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c9726a33d
commit
d53eea443e
@ -10,12 +10,35 @@ const HOSTER_SETTINGS_DEFAULTS = {
|
||||
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 = {
|
||||
hosters: {
|
||||
'doodstream.com': { enabled: true, apiKey: '', username: '', password: '' },
|
||||
'voe.sx': { enabled: true, apiKey: '' },
|
||||
'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' },
|
||||
'byse.sx': { enabled: true, apiKey: '' }
|
||||
'doodstream.com': [],
|
||||
'voe.sx': [],
|
||||
'vidmoly.me': [],
|
||||
'byse.sx': []
|
||||
},
|
||||
hosterSettings: {
|
||||
'doodstream.com': { ...HOSTER_SETTINGS_DEFAULTS },
|
||||
@ -113,13 +136,46 @@ class ConfigStore {
|
||||
}
|
||||
if (!data) return JSON.parse(JSON.stringify(DEFAULTS));
|
||||
|
||||
// Merge with defaults so new hosters are always present
|
||||
const hosters = { ...DEFAULTS.hosters };
|
||||
// Migrate old single-object format to array format
|
||||
for (const [name, val] of Object.entries(data.hosters || {})) {
|
||||
if (hosters[name]) {
|
||||
hosters[name] = { ...hosters[name], ...val };
|
||||
if (val && !Array.isArray(val)) {
|
||||
if (!val.id) val.id = `${name}-migrated-${Date.now()}`;
|
||||
// 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
|
||||
const hosterSettings = {};
|
||||
for (const name of Object.keys(DEFAULTS.hosterSettings)) {
|
||||
@ -196,3 +252,6 @@ class 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;
|
||||
|
||||
@ -37,6 +37,12 @@ class UploadManager extends EventEmitter {
|
||||
this.lastStartTime = {}; // hoster -> timestamp of last upload start
|
||||
this.intervalLocks = {}; // hoster -> Promise chain for serialized interval waits
|
||||
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) {
|
||||
@ -364,22 +370,7 @@ class UploadManager extends EventEmitter {
|
||||
});
|
||||
};
|
||||
|
||||
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 result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle);
|
||||
|
||||
const elapsed = Math.round((Date.now() - jobStart) / 1000);
|
||||
this.sessionBytes += fileSize;
|
||||
@ -432,6 +423,83 @@ class UploadManager extends EventEmitter {
|
||||
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';
|
||||
emitFinalStatus('error', { error });
|
||||
recordFinalResult('error', { error });
|
||||
@ -453,6 +521,24 @@ 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) {
|
||||
this.emit('progress', { uploadId, fileName, hoster, ...data });
|
||||
}
|
||||
|
||||
241
main.js
241
main.js
@ -115,82 +115,68 @@ function appendUploadLog(hoster, link, fileName) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function buildUploadTasks(config, files, hosters) {
|
||||
const tasks = [];
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
for (const hoster of hosters) {
|
||||
const hosterConfig = config.hosters[hoster];
|
||||
if (!hosterConfig) {
|
||||
debugLog(` skip ${hoster}: no config`);
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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)}...`);
|
||||
}
|
||||
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) {
|
||||
const tasks = [];
|
||||
for (const file of files) {
|
||||
for (const hoster of hosters) {
|
||||
const account = getPrimaryAccount(config, hoster);
|
||||
if (!account) { debugLog(` skip ${hoster}: no enabled account with creds`); continue; }
|
||||
tasks.push(buildTaskFromAccount(hoster, account, { file }));
|
||||
}
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
function buildUploadTasksFromJobs(config, jobs) {
|
||||
if (!Array.isArray(jobs)) return [];
|
||||
|
||||
return jobs.flatMap((job) => {
|
||||
if (!job || !job.file || !job.hoster) return [];
|
||||
const hosterConfig = config.hosters[job.hoster];
|
||||
if (!hosterConfig) {
|
||||
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 }];
|
||||
const account = getPrimaryAccount(config, job.hoster);
|
||||
if (!account) { debugLog(` skip ${job.hoster}: no enabled account`); return []; }
|
||||
return [buildTaskFromAccount(job.hoster, account, { file: job.file, jobId: job.id || job.jobId || null })];
|
||||
});
|
||||
}
|
||||
|
||||
@ -360,6 +346,13 @@ async function checkByseHealth(hosterConfig) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return { status: 'error', message: msg };
|
||||
}
|
||||
@ -367,75 +360,69 @@ async function checkByseHealth(hosterConfig) {
|
||||
return { status: 'error', message: 'API Key ungültig oder Server nicht erreichbar' };
|
||||
}
|
||||
|
||||
async function runHosterHealthCheck(config, requestedHosters) {
|
||||
// requestedChecks can be:
|
||||
// - 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 source = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
||||
? requestedHosters
|
||||
: allowed;
|
||||
|
||||
const hosters = source
|
||||
.map((name) => String(name || '').trim())
|
||||
.filter((name, index, arr) => name && arr.indexOf(name) === index);
|
||||
// Normalize input to [{ hoster, accountId? }]
|
||||
let checks;
|
||||
if (!Array.isArray(requestedChecks) || requestedChecks.length === 0) {
|
||||
// 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 checks = hosters.map(async (hoster) => {
|
||||
const results = await Promise.all(checks.map(async ({ hoster, accountId }) => {
|
||||
if (!allowed.includes(hoster)) {
|
||||
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||
}
|
||||
|
||||
const hosterConfig = config && config.hosters ? config.hosters[hoster] : null;
|
||||
// Find specific account
|
||||
const accounts = config.hosters[hoster];
|
||||
const hosterConfig = Array.isArray(accounts) ? accounts.find(a => a.id === accountId) : null;
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (hoster === 'doodstream.com') {
|
||||
const result = await withTimeout(
|
||||
checkDoodstreamHealth(hosterConfig),
|
||||
HEALTH_CHECK_TIMEOUT,
|
||||
'Doodstream-Check'
|
||||
);
|
||||
return { hoster, ...result };
|
||||
result = await withTimeout(checkDoodstreamHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Doodstream-Check');
|
||||
} else if (hoster === 'vidmoly.me') {
|
||||
result = await withTimeout(checkVidmolyHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Vidmoly-Check');
|
||||
} else if (hoster === 'voe.sx') {
|
||||
result = await withTimeout(checkVoeHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'VOE-Check');
|
||||
} else if (hoster === 'byse.sx') {
|
||||
result = await withTimeout(checkByseHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Byse-Check');
|
||||
} else {
|
||||
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||
}
|
||||
|
||||
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' };
|
||||
return { hoster, accountId, ...result };
|
||||
} catch (err) {
|
||||
return {
|
||||
hoster,
|
||||
status: 'error',
|
||||
message: err && err.message ? err.message : 'Health-Check fehlgeschlagen'
|
||||
};
|
||||
return { hoster, accountId, status: 'error', message: err && err.message ? err.message : 'Health-Check fehlgeschlagen' };
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
const results = await Promise.all(checks);
|
||||
return {
|
||||
checkedAt: new Date().toISOString(),
|
||||
results
|
||||
};
|
||||
return { checkedAt: new Date().toISOString(), results };
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
@ -680,6 +667,20 @@ 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) => {
|
||||
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
||||
await configStore.appendHistory(summary);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "1.9.8",
|
||||
"version": "2.0.0",
|
||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@ -64,6 +64,11 @@ contextBridge.exposeInMainWorld('api', {
|
||||
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
|
||||
showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
|
||||
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
|
||||
@ -99,5 +104,6 @@ contextBridge.exposeInMainWorld('api', {
|
||||
ipcRenderer.removeAllListeners('shutdown-countdown');
|
||||
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
||||
ipcRenderer.removeAllListeners('drop-target:files');
|
||||
ipcRenderer.removeAllListeners('account-switched');
|
||||
}
|
||||
});
|
||||
|
||||
528
renderer/app.js
528
renderer/app.js
@ -1,5 +1,15 @@
|
||||
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 ---
|
||||
let selectedFiles = []; // { path, name, size }
|
||||
let selectedUploadHosters = [];
|
||||
@ -7,8 +17,8 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
|
||||
let hosterSettings = {};
|
||||
let uploading = false;
|
||||
let healthCheckRunning = false;
|
||||
let accountStatuses = {}; // { 'voe.sx': { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } }
|
||||
let editingAccountHoster = null; // null = adding, string = editing
|
||||
let accountStatuses = {}; // { accountId: { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } }
|
||||
let editingAccountId = null; // null = adding, string = editing account by ID
|
||||
let autoHealthCheckEnabled = true;
|
||||
let queuePersistTimer = null;
|
||||
let settingsSaveTimer = null;
|
||||
@ -108,6 +118,11 @@ 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
|
||||
window.api.onDropTargetFiles((paths) => {
|
||||
addPathsToQueue(paths);
|
||||
@ -136,19 +151,26 @@ document.querySelectorAll('.tab').forEach(tab => {
|
||||
});
|
||||
|
||||
// --- Hoster selection ---
|
||||
function hosterHasCredentials(name, hoster) {
|
||||
if (name === 'vidmoly.me') return !!(hoster.username && hoster.password);
|
||||
if (name === 'voe.sx' || name === 'doodstream.com') return !!(hoster.username && hoster.password) || !!hoster.apiKey;
|
||||
return !!hoster.apiKey;
|
||||
function accountHasCreds(name, account) {
|
||||
if (!account) return false;
|
||||
if (account.authType === 'api') return !!account.apiKey;
|
||||
if (account.authType === 'login') return !!(account.username && account.password);
|
||||
// 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() {
|
||||
return HOSTERS
|
||||
.map(name => {
|
||||
const hoster = config.hosters[name] || {};
|
||||
return { name, hoster, hasCreds: hosterHasCredentials(name, hoster) };
|
||||
})
|
||||
.filter(item => item.hasCreds && item.hoster.enabled !== false);
|
||||
const result = [];
|
||||
for (const name of HOSTERS) {
|
||||
const accounts = config.hosters[name];
|
||||
if (!Array.isArray(accounts)) continue;
|
||||
const hasEnabledAccount = accounts.some(a => a.enabled !== false && accountHasCreds(name, a));
|
||||
if (hasEnabledAccount) result.push({ name });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function syncSelectedUploadHosters() {
|
||||
@ -156,8 +178,8 @@ function syncSelectedUploadHosters() {
|
||||
selectedUploadHosters = selectedUploadHosters.filter(name => available.has(name));
|
||||
if (selectedUploadHosters.length === 0) {
|
||||
selectedUploadHosters = HOSTERS.filter(name => {
|
||||
const hoster = config.hosters[name] || {};
|
||||
return !!hoster.enabled && hosterHasCredentials(name, hoster);
|
||||
const accounts = config.hosters[name];
|
||||
return Array.isArray(accounts) && accounts.some(a => a.enabled !== false && accountHasCreds(name, a));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -176,24 +198,17 @@ function getHosterLabel(name) {
|
||||
return labels[name] || name;
|
||||
}
|
||||
|
||||
function getAccountModeParts(name, hoster) {
|
||||
if (!hoster) return [];
|
||||
const hasLogin = !!(hoster.username && hoster.password);
|
||||
const hasApi = !!hoster.apiKey;
|
||||
|
||||
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 getAccountAuthLabel(account) {
|
||||
if (!account) return '';
|
||||
if (account.authType === 'api') return 'API';
|
||||
if (account.authType === 'login') return 'Web Login';
|
||||
return '';
|
||||
}
|
||||
|
||||
function getAccountDisplayName(name, hoster) {
|
||||
const parts = getAccountModeParts(name, hoster);
|
||||
return parts.length > 0
|
||||
? `${getHosterLabel(name)} (${parts.join(' + ')})`
|
||||
function getAccountDisplayName(name, account) {
|
||||
const authLabel = getAccountAuthLabel(account);
|
||||
return authLabel
|
||||
? `${getHosterLabel(name)} (${authLabel})`
|
||||
: getHosterLabel(name);
|
||||
}
|
||||
|
||||
@ -206,14 +221,43 @@ function maskCredential(value, keep = 4) {
|
||||
|
||||
function ensureAccountStatusEntries() {
|
||||
const nextStatuses = {};
|
||||
for (const { name } of getAccountsWithCreds()) {
|
||||
nextStatuses[name] = accountStatuses[name] || { status: 'unchecked', message: '' };
|
||||
for (const { account } of getAllAccountsFlat()) {
|
||||
if (account.id) {
|
||||
nextStatuses[account.id] = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
||||
}
|
||||
}
|
||||
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() {
|
||||
const accounts = getAccountsWithCreds();
|
||||
const accounts = getAccountsWithCredsFlat();
|
||||
if (!accounts.length) return;
|
||||
setTimeout(() => {
|
||||
runHealthCheck('startup').catch(() => {});
|
||||
@ -227,7 +271,7 @@ function renderHosterSummary() {
|
||||
if (hosters.length === 0) {
|
||||
summary.textContent = 'Keine Upload-Ziele ausgewählt';
|
||||
} else if (hosters.length === 1) {
|
||||
summary.textContent = `Aktives Ziel: ${getAccountDisplayName(hosters[0], config.hosters[hosters[0]] || {})}`;
|
||||
summary.textContent = `Aktives Ziel: ${getHosterLabel(hosters[0])}`;
|
||||
} else {
|
||||
summary.textContent = `${hosters.length} Ziele aktiv: ${hosters.map((name) => getHosterLabel(name)).join(', ')}`;
|
||||
}
|
||||
@ -247,17 +291,21 @@ function renderHosterModal() {
|
||||
|
||||
list.innerHTML = available.map(item => {
|
||||
const checked = selectedUploadHosters.includes(item.name);
|
||||
const h = config.hosters[item.name] || {};
|
||||
const st = accountStatuses[item.name];
|
||||
const subtitle = st && st.status === 'ok' ? 'Bereit'
|
||||
: st && st.status === 'warn' ? 'Prüfung mit Warnung'
|
||||
: st && st.status === 'error' ? 'Login-Fehler'
|
||||
: `${getCredentialLabel(item.name, h)} hinterlegt`;
|
||||
// Get first enabled account's status for subtitle
|
||||
const accounts = config.hosters[item.name] || [];
|
||||
const enabledAccounts = accounts.filter(a => a.enabled !== false && accountHasCreds(item.name, a));
|
||||
const accountCount = enabledAccounts.length;
|
||||
let subtitle = `${accountCount} Account${accountCount !== 1 ? 's' : ''}`;
|
||||
// Check if any account has ok status
|
||||
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 `
|
||||
<label class="hoster-option${checked ? ' selected' : ''}" data-hoster-option="${item.name}">
|
||||
<input type="checkbox" data-hoster-modal="${item.name}" ${checked ? 'checked' : ''}>
|
||||
<div class="hoster-option-main">
|
||||
<div class="hoster-option-title">${escapeHtml(getAccountDisplayName(item.name, h))}</div>
|
||||
<div class="hoster-option-title">${escapeHtml(getHosterLabel(item.name))}</div>
|
||||
<div class="hoster-option-subtitle">${subtitle}</div>
|
||||
</div>
|
||||
</label>
|
||||
@ -1459,7 +1507,7 @@ async function retrySelectedJobs() {
|
||||
const retryJobs = [];
|
||||
queueJobs.forEach(j => {
|
||||
if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) {
|
||||
j.status = 'preview';
|
||||
j.status = uploading ? 'queued' : 'preview';
|
||||
j.error = null;
|
||||
j.result = null;
|
||||
j.bytesUploaded = 0;
|
||||
@ -1720,11 +1768,14 @@ async function executeHealthCheck(hosters, mode) {
|
||||
const result = await window.api.runHealthCheck({ hosters });
|
||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||
rows.forEach((row) => {
|
||||
if (!row || !row.hoster) return;
|
||||
accountStatuses[row.hoster] = {
|
||||
status: row.status || 'unchecked',
|
||||
message: row.message || ''
|
||||
};
|
||||
if (!row) return;
|
||||
const key = row.accountId || row.hoster;
|
||||
if (key) {
|
||||
accountStatuses[key] = {
|
||||
status: row.status || 'unchecked',
|
||||
message: row.message || ''
|
||||
};
|
||||
}
|
||||
});
|
||||
renderHealthCheckResults(rows);
|
||||
renderAccounts();
|
||||
@ -1734,17 +1785,25 @@ async function executeHealthCheck(hosters, mode) {
|
||||
|
||||
async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
||||
if (healthCheckRunning || (uploading && mode === 'manual')) return [];
|
||||
const hosters = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
||||
? requestedHosters
|
||||
: HOSTERS.filter((name) => hosterHasCredentials(name, config.hosters[name] || {}));
|
||||
// Build check list: all enabled accounts with creds
|
||||
let hosters;
|
||||
if (Array.isArray(requestedHosters) && requestedHosters.length > 0) {
|
||||
hosters = requestedHosters;
|
||||
} else {
|
||||
hosters = getAccountsWithCredsFlat()
|
||||
.filter(({ account }) => account.enabled !== false)
|
||||
.map(({ name, account }) => ({ hoster: name, accountId: account.id }));
|
||||
}
|
||||
if (hosters.length === 0) {
|
||||
if (mode === 'manual') alert('Keine Hoster mit Zugangsdaten für einen Check.');
|
||||
return [];
|
||||
}
|
||||
healthCheckRunning = true;
|
||||
hosters.forEach((hoster) => {
|
||||
accountStatuses[hoster] = { status: 'checking', message: '' };
|
||||
});
|
||||
// Mark all accounts as checking
|
||||
for (const h of hosters) {
|
||||
const key = typeof h === 'string' ? h : (h.accountId || h.hoster);
|
||||
accountStatuses[key] = { status: 'checking', message: '' };
|
||||
}
|
||||
renderAccounts();
|
||||
try {
|
||||
return await executeHealthCheck(hosters, mode);
|
||||
@ -1763,7 +1822,7 @@ function renderSettings() {
|
||||
container.innerHTML = '';
|
||||
|
||||
const globalSettings = config.globalSettings || {};
|
||||
const configuredAccounts = getAccountsWithCreds();
|
||||
const configuredAccounts = getAvailableHosters();
|
||||
const generalPanel = document.createElement('div');
|
||||
generalPanel.className = 'hoster-settings-panel';
|
||||
generalPanel.innerHTML = `
|
||||
@ -1969,7 +2028,7 @@ function renderSettings() {
|
||||
container.appendChild(empty);
|
||||
}
|
||||
|
||||
for (const { name, hoster } of configuredAccounts) {
|
||||
for (const { name } of configuredAccounts) {
|
||||
const hs = hosterSettings[name] || {};
|
||||
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? (hs.maxSpeedKbs / 1024).toFixed(2).replace(/\.00$/, '') : '0';
|
||||
|
||||
@ -1979,7 +2038,7 @@ function renderSettings() {
|
||||
panel.innerHTML = `
|
||||
<div class="hoster-panel-header" data-hoster="${name}">
|
||||
<span class="panel-arrow">▶</span>
|
||||
<span class="panel-title">${escapeHtml(getAccountDisplayName(name, hoster))}</span>
|
||||
<span class="panel-title">${escapeHtml(getHosterLabel(name))}</span>
|
||||
<span class="panel-status active">Aktiv</span>
|
||||
</div>
|
||||
<div class="hoster-panel-body" data-panel="${name}" style="display:none">
|
||||
@ -2155,25 +2214,14 @@ async function saveSettings(options = {}) {
|
||||
}
|
||||
|
||||
// --- Accounts ---
|
||||
function getAccountsWithCreds() {
|
||||
return HOSTERS
|
||||
.map(name => ({ name, hoster: config.hosters[name] || {} }))
|
||||
.filter(item => hosterHasCredentials(item.name, item.hoster));
|
||||
}
|
||||
|
||||
function getHostersWithoutCreds() {
|
||||
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 getCredentialLabel(name, account) {
|
||||
if (!account) return 'Keine Zugangsdaten';
|
||||
if (account.authType === 'api') return `API: ${maskCredential(account.apiKey) || 'nicht gesetzt'}`;
|
||||
if (account.authType === 'login') return `Login: ${account.username || 'nicht gesetzt'}`;
|
||||
// Fallback
|
||||
if (account.username && account.password) return `Login: ${account.username}`;
|
||||
if (account.apiKey) return `API: ${maskCredential(account.apiKey)}`;
|
||||
return 'Keine Zugangsdaten';
|
||||
}
|
||||
|
||||
function renderAccounts() {
|
||||
@ -2181,11 +2229,11 @@ function renderAccounts() {
|
||||
if (!container) return;
|
||||
ensureAccountStatusEntries();
|
||||
|
||||
const accounts = getAccountsWithCreds();
|
||||
const allAccounts = getAllAccountsFlat();
|
||||
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
|
||||
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
|
||||
|
||||
if (accounts.length === 0) {
|
||||
if (allAccounts.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="accounts-empty">
|
||||
<p>Keine Accounts vorhanden</p>
|
||||
@ -2194,33 +2242,51 @@ function renderAccounts() {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = accounts.map(({ name, hoster }) => {
|
||||
const isDisabled = hoster.enabled === false;
|
||||
const st = accountStatuses[name] || { status: 'unchecked', message: '' };
|
||||
const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
|
||||
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';
|
||||
// Group by hoster for drag reorder sections
|
||||
const byHoster = {};
|
||||
for (const { name, account } of allAccounts) {
|
||||
if (!byHoster[name]) byHoster[name] = [];
|
||||
byHoster[name].push(account);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="account-card${isDisabled ? ' account-disabled' : ''}" data-account="${name}">
|
||||
<div class="account-card-info">
|
||||
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, hoster))}</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="${name}">${toggleLabel}</button>
|
||||
<button class="btn btn-xs btn-secondary" data-account-check="${name}" ${isDisabled ? 'disabled' : ''}>Prüfen</button>
|
||||
<button class="btn btn-xs btn-secondary" data-account-edit="${name}">Bearbeiten</button>
|
||||
<button class="btn btn-xs btn-danger" data-account-delete="${name}">Löschen</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
let html = '';
|
||||
for (const name of HOSTERS) {
|
||||
const accounts = byHoster[name];
|
||||
if (!accounts || accounts.length === 0) continue;
|
||||
html += `<div class="account-hoster-group" data-hoster-group="${name}">
|
||||
<div class="account-hoster-group-title">${escapeHtml(getHosterLabel(name))}</div>`;
|
||||
accounts.forEach((account, idx) => {
|
||||
const isDisabled = account.enabled === false;
|
||||
const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
||||
const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
|
||||
const statusLabel = isDisabled ? 'Deaktiviert' : (statusLabels[st.status] || 'Nicht geprüft');
|
||||
const statusClass = isDisabled ? 'disabled' : st.status;
|
||||
const credLabel = getCredentialLabel(name, account);
|
||||
const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
|
||||
const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`;
|
||||
|
||||
html += `
|
||||
<div class="account-card${isDisabled ? ' account-disabled' : ''}" data-account-id="${account.id}" data-account-hoster="${name}" draggable="true">
|
||||
<div class="account-card-drag-handle" title="Ziehen zum Sortieren">☰</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
|
||||
container.querySelectorAll('[data-account-toggle]').forEach(btn => {
|
||||
@ -2235,14 +2301,73 @@ function renderAccounts() {
|
||||
container.querySelectorAll('[data-account-check]').forEach(btn => {
|
||||
btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
|
||||
});
|
||||
|
||||
// Drag-and-drop reorder within each hoster group
|
||||
setupAccountDragReorder(container);
|
||||
}
|
||||
|
||||
async function toggleAccount(hosterName) {
|
||||
const hosters = { ...config.hosters };
|
||||
const hoster = { ...hosters[hosterName] };
|
||||
hoster.enabled = hoster.enabled === false ? true : false;
|
||||
hosters[hosterName] = hoster;
|
||||
await window.api.saveConfig({ hosters });
|
||||
function setupAccountDragReorder(container) {
|
||||
let draggedCard = null;
|
||||
container.querySelectorAll('.account-card[draggable]').forEach(card => {
|
||||
card.addEventListener('dragstart', (e) => {
|
||||
draggedCard = card;
|
||||
card.classList.add('dragging');
|
||||
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();
|
||||
syncSelectedUploadHosters();
|
||||
renderAccounts();
|
||||
@ -2251,66 +2376,51 @@ async function toggleAccount(hosterName) {
|
||||
renderSettings();
|
||||
}
|
||||
|
||||
async function checkSingleAccount(hosterName) {
|
||||
if (!hosterName || healthCheckRunning) return;
|
||||
async function checkSingleAccount(accountId) {
|
||||
if (!accountId || healthCheckRunning) return;
|
||||
const found = findAccountById(accountId);
|
||||
if (!found) return;
|
||||
healthCheckRunning = true;
|
||||
accountStatuses[hosterName] = { status: 'checking', message: '' };
|
||||
accountStatuses[accountId] = { status: 'checking', message: '' };
|
||||
renderAccounts();
|
||||
try {
|
||||
const rows = await executeHealthCheck([hosterName], 'manual');
|
||||
const row = rows.find(r => r.hoster === hosterName);
|
||||
if (row) accountStatuses[hosterName] = { status: row.status || 'error', message: row.message || '' };
|
||||
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] });
|
||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||
const row = rows.find(r => r.accountId === accountId);
|
||||
if (row) accountStatuses[accountId] = { status: row.status || 'error', message: row.message || '' };
|
||||
} catch (err) {
|
||||
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||
} finally {
|
||||
healthCheckRunning = false;
|
||||
}
|
||||
renderAccounts();
|
||||
}
|
||||
|
||||
function getCredsFieldsHtml(name, hoster) {
|
||||
hoster = hoster || {};
|
||||
if (name === 'vidmoly.me') {
|
||||
function getCredsFieldsHtml(authType, account) {
|
||||
account = account || {};
|
||||
if (authType === 'login') {
|
||||
return `
|
||||
<div class="settings-row">
|
||||
<label>Username</label>
|
||||
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="Username">
|
||||
<label>Username / E-Mail</label>
|
||||
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(account.username || '')}" placeholder="Username oder E-Mail">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label>Passwort</label>
|
||||
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort">
|
||||
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
|
||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||
</div>`;
|
||||
}
|
||||
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">👁</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">👁</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
|
||||
// API key
|
||||
return `
|
||||
<div class="settings-row">
|
||||
<label>API Key</label>
|
||||
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key">
|
||||
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(account.apiKey || '')}" placeholder="API Key">
|
||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function openAccountModal(editHoster) {
|
||||
editingAccountHoster = editHoster || null;
|
||||
function openAccountModal(editAccountId) {
|
||||
editingAccountId = editAccountId || null;
|
||||
const modal = document.getElementById('accountModal');
|
||||
const title = document.getElementById('accountModalTitle');
|
||||
const subtitle = document.getElementById('accountModalSubtitle');
|
||||
@ -2323,28 +2433,26 @@ function openAccountModal(editHoster) {
|
||||
statusEl.textContent = '';
|
||||
statusEl.className = 'account-modal-status';
|
||||
|
||||
if (editingAccountHoster) {
|
||||
if (editingAccountId) {
|
||||
// Edit mode
|
||||
const found = findAccountById(editingAccountId);
|
||||
if (!found) return;
|
||||
title.textContent = 'Account bearbeiten';
|
||||
subtitle.textContent = `Zugangsdaten für ${getAccountDisplayName(editingAccountHoster, config.hosters[editingAccountHoster] || {})} bearbeiten.`;
|
||||
subtitle.textContent = `Zugangsdaten für ${getAccountDisplayName(found.name, found.account)} bearbeiten.`;
|
||||
hosterRow.style.display = 'none';
|
||||
saveBtn.textContent = 'Speichern & prüfen';
|
||||
const hoster = config.hosters[editingAccountHoster] || {};
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(editingAccountHoster, hoster);
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(found.account.authType || 'login', found.account);
|
||||
} else {
|
||||
// Add mode
|
||||
// Add mode — always show all options (multiple accounts per hoster allowed)
|
||||
title.textContent = 'Account hinzufügen';
|
||||
subtitle.textContent = 'Wähle einen Hoster und gib deine Zugangsdaten ein.';
|
||||
hosterRow.style.display = 'flex';
|
||||
saveBtn.textContent = 'Anlegen & prüfen';
|
||||
const available = getHostersWithoutCreds();
|
||||
if (available.length === 0) {
|
||||
hosterSelect.innerHTML = '<option value="">Alle Hoster bereits eingerichtet</option>';
|
||||
credsContainer.innerHTML = '';
|
||||
} else {
|
||||
hosterSelect.innerHTML = available.map(name => `<option value="${name}">${getHosterLabel(name)}</option>`).join('');
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(available[0], {});
|
||||
}
|
||||
hosterSelect.innerHTML = HOSTER_ADD_OPTIONS.map(opt =>
|
||||
`<option value="${opt.value}">${escapeHtml(opt.label)}</option>`
|
||||
).join('');
|
||||
const firstOpt = HOSTER_ADD_OPTIONS[0];
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {});
|
||||
}
|
||||
|
||||
// Toggle visibility buttons
|
||||
@ -2360,14 +2468,16 @@ function openAccountModal(editHoster) {
|
||||
|
||||
function closeAccountModal() {
|
||||
document.getElementById('accountModal').style.display = 'none';
|
||||
editingAccountHoster = null;
|
||||
editingAccountId = null;
|
||||
}
|
||||
|
||||
function openDeleteAccountModal(hosterName) {
|
||||
function openDeleteAccountModal(accountId) {
|
||||
const found = findAccountById(accountId);
|
||||
if (!found) return;
|
||||
const modal = document.getElementById('deleteAccountModal');
|
||||
const msg = document.getElementById('deleteAccountMessage');
|
||||
msg.textContent = `Account für "${getHosterLabel(hosterName)}" wirklich löschen? Alle Zugangsdaten werden entfernt.`;
|
||||
modal.dataset.hoster = hosterName;
|
||||
msg.textContent = `Account "${getAccountDisplayName(found.name, found.account)}" wirklich löschen?`;
|
||||
modal.dataset.accountId = accountId;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
@ -2375,22 +2485,20 @@ function closeDeleteModal() {
|
||||
document.getElementById('deleteAccountModal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function deleteAccount(hosterName) {
|
||||
const hosters = { ...config.hosters };
|
||||
// Reset credentials to defaults
|
||||
if (hosterName === 'vidmoly.me') {
|
||||
hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
|
||||
} else if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
|
||||
hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
|
||||
} else {
|
||||
hosters[hosterName] = { enabled: false, apiKey: '' };
|
||||
async function deleteAccount(accountId) {
|
||||
const found = findAccountById(accountId);
|
||||
if (!found) return;
|
||||
// Remove account from the array
|
||||
const accounts = config.hosters[found.name];
|
||||
if (Array.isArray(accounts)) {
|
||||
config.hosters[found.name] = accounts.filter(a => a.id !== accountId);
|
||||
}
|
||||
delete accountStatuses[hosterName];
|
||||
await window.api.saveConfig({ hosters });
|
||||
delete accountStatuses[accountId];
|
||||
await window.api.saveConfig({ hosters: config.hosters });
|
||||
config = await window.api.getConfig();
|
||||
ensureAccountStatusEntries();
|
||||
syncSelectedUploadHosters();
|
||||
if (getAccountsWithCreds().length === 0) renderHealthCheckResults([]);
|
||||
if (getAllAccountsFlat().length === 0) renderHealthCheckResults([]);
|
||||
renderAccounts();
|
||||
renderHosterSummary();
|
||||
renderHosterModal();
|
||||
@ -2398,27 +2506,39 @@ async function deleteAccount(hosterName) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
|
||||
function readAccountCredsFromModal(hosterName) {
|
||||
if (hosterName === 'vidmoly.me') {
|
||||
function readAccountCredsFromModal(authType) {
|
||||
if (authType === 'login') {
|
||||
const username = (document.getElementById('accField_username')?.value || '').trim();
|
||||
const password = (document.getElementById('accField_password')?.value || '').trim();
|
||||
return { enabled: !!(username && password), authType: 'login', username, password };
|
||||
}
|
||||
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 };
|
||||
}
|
||||
// API
|
||||
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
|
||||
return { enabled: !!apiKey, apiKey };
|
||||
return { enabled: !!apiKey, authType: 'api', apiKey };
|
||||
}
|
||||
|
||||
async function saveAccount() {
|
||||
const hosterName = editingAccountHoster || document.getElementById('accountHosterSelect')?.value;
|
||||
if (!hosterName) return;
|
||||
let hosterName, authType, accountId;
|
||||
|
||||
const creds = readAccountCredsFromModal(hosterName);
|
||||
if (editingAccountId) {
|
||||
// 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) {
|
||||
const statusEl = document.getElementById('accountModalStatus');
|
||||
statusEl.textContent = 'Bitte Zugangsdaten eingeben.';
|
||||
@ -2427,9 +2547,18 @@ async function saveAccount() {
|
||||
}
|
||||
|
||||
// Save credentials
|
||||
const hosters = { ...config.hosters };
|
||||
hosters[hosterName] = creds;
|
||||
await window.api.saveConfig({ hosters });
|
||||
if (!Array.isArray(config.hosters[hosterName])) config.hosters[hosterName] = [];
|
||||
if (editingAccountId) {
|
||||
// Update existing account in array
|
||||
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();
|
||||
|
||||
// Show checking status
|
||||
@ -2439,30 +2568,31 @@ async function saveAccount() {
|
||||
statusEl.className = 'account-modal-status checking';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
accountStatuses[hosterName] = { status: 'checking', message: '' };
|
||||
accountStatuses[accountId] = { status: 'checking', message: '' };
|
||||
syncSelectedUploadHosters();
|
||||
renderAccounts();
|
||||
renderHosterSummary();
|
||||
renderHosterModal();
|
||||
renderSettings();
|
||||
|
||||
// Run health check
|
||||
// Run health check for this specific account
|
||||
try {
|
||||
const rows = await executeHealthCheck([hosterName], 'auto');
|
||||
const row = rows.find(r => r.hoster === hosterName);
|
||||
const result = await window.api.runHealthCheck({ hosters: [{ hoster: hosterName, accountId }] });
|
||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||
const row = rows.find(r => r.accountId === accountId);
|
||||
if (row && (row.status === 'ok' || row.status === 'warn')) {
|
||||
accountStatuses[hosterName] = { status: row.status || 'ok', message: row.message || '' };
|
||||
accountStatuses[accountId] = { status: row.status || 'ok', message: row.message || '' };
|
||||
statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!';
|
||||
statusEl.className = 'account-modal-status ok';
|
||||
setTimeout(() => closeAccountModal(), 1200);
|
||||
} else {
|
||||
const msg = (row && row.message) || 'Login fehlgeschlagen';
|
||||
accountStatuses[hosterName] = { status: 'error', message: msg };
|
||||
accountStatuses[accountId] = { status: 'error', message: msg };
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'account-modal-status error';
|
||||
}
|
||||
} catch (err) {
|
||||
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||
statusEl.textContent = err.message || 'Prüfung fehlgeschlagen';
|
||||
statusEl.className = 'account-modal-status error';
|
||||
} finally {
|
||||
@ -2822,8 +2952,10 @@ function setupListeners() {
|
||||
|
||||
// Account hoster select change → update credential fields
|
||||
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');
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(e.target.value, {});
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(authType, {});
|
||||
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const input = btn.previousElementSibling;
|
||||
@ -2839,8 +2971,8 @@ function setupListeners() {
|
||||
document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
|
||||
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
|
||||
const modal = document.getElementById('deleteAccountModal');
|
||||
const hoster = modal.dataset.hoster;
|
||||
if (hoster) deleteAccount(hoster);
|
||||
const accountId = modal.dataset.accountId;
|
||||
if (accountId) deleteAccount(accountId);
|
||||
});
|
||||
document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'deleteAccountModal') closeDeleteModal();
|
||||
|
||||
@ -812,6 +812,44 @@ body {
|
||||
.account-modal-status.ok { color: var(--success); }
|
||||
.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 {
|
||||
text-align: center;
|
||||
padding: 48px 16px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user