Tauri 2 / Rust rewrite — initial 2.0 scaffold
Working: - Core: config, secret encryption, events, throttle - Upload manager with full rotation/classifier parity to v1 - Clouddrop uploader (simple + chunked upload.clouddrop.cc) - Byse uploader with file-list polling for empty-filecode case - Vidmoly uploader (new /api/auth/login + /api/upload/config + X-Progress-ID) - Minimal frontend (accounts, settings, upload table, rotation log) - Release build: exe 6.9 MB, NSIS installer 2.5 MB, MSI 3.4 MB Stubs (return 'not yet ported' error): - Doodstream (web login + CSRF — v1 scraper needs careful port) - VOE (web login + CSRF + delivery-node negotiation) Not yet migrated from v1: - Queue persistence on restart - Folder monitor - Remote-control server - Drop-target floating window - Auto-updater
This commit is contained in:
+463
@@ -0,0 +1,463 @@
|
||||
// Multi-Hoster-Upload 2.0 — minimal frontend demonstrating the Tauri bridge.
|
||||
// Uses the global Tauri runtime (withGlobalTauri: true).
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
const dialog = window.__TAURI__.dialog;
|
||||
|
||||
let config = null;
|
||||
let selectedFiles = [];
|
||||
let selectedHosters = [];
|
||||
let queueJobs = [];
|
||||
const jobById = new Map();
|
||||
let uploading = false;
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function escHtml(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' })[c]); }
|
||||
function uuid() { return 'j-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }
|
||||
|
||||
function showToast(msg, ms) {
|
||||
const el = $('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
clearTimeout(showToast._t);
|
||||
showToast._t = setTimeout(() => el.classList.remove('show'), ms || 2200);
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
(function () {
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
const views = document.querySelectorAll('.view');
|
||||
let active = document.querySelector('.tab.active');
|
||||
tabs.forEach(function (t) {
|
||||
t.addEventListener('click', function () {
|
||||
if (t === active) return;
|
||||
if (active) active.classList.remove('active');
|
||||
t.classList.add('active');
|
||||
views.forEach(function (v) { v.classList.toggle('active', v.id === t.dataset.view + '-view'); });
|
||||
active = t;
|
||||
if (t.dataset.view === 'log') refreshLog();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
async function loadConfig() {
|
||||
config = await invoke('get_config');
|
||||
renderAccounts();
|
||||
renderHosterCheckboxes();
|
||||
renderSettings();
|
||||
}
|
||||
|
||||
function hosterLabel(h) {
|
||||
const map = { 'clouddrop.cc': 'Clouddrop', 'byse.sx': 'Byse', 'vidmoly.me': 'Vidmoly',
|
||||
'doodstream.com': 'Doodstream', 'voe.sx': 'VOE' };
|
||||
return map[h] || h;
|
||||
}
|
||||
function accountLabel(h, a) {
|
||||
return a.label || a.username || (a.api_key ? 'API ' + String(a.api_key).slice(0, 8) + String.fromCharCode(8230) : 'Account ' + String(a.id).slice(-6));
|
||||
}
|
||||
function hasCreds(_h, a) {
|
||||
if (a.auth_type === 'api') return !!a.api_key;
|
||||
if (a.auth_type === 'login') return !!(a.username && a.password);
|
||||
return !!a.api_key || !!(a.username && a.password);
|
||||
}
|
||||
function statusLabel(s) {
|
||||
const map = { preview: 'Vorschau', queued: 'Wartet', 'getting-server': 'Server...', uploading: 'Upload',
|
||||
retrying: 'Retry', done: 'Fertig', error: 'Fehler', aborted: 'Abgebrochen' };
|
||||
return map[s] || s;
|
||||
}
|
||||
|
||||
// Accounts
|
||||
function renderAccounts() {
|
||||
const list = $('accountsList');
|
||||
while (list.firstChild) list.removeChild(list.firstChild);
|
||||
if (!config || !config.hosters) {
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Kein Config geladen.';
|
||||
list.appendChild(p);
|
||||
return;
|
||||
}
|
||||
let anyAccount = false;
|
||||
for (const hoster of Object.keys(config.hosters)) {
|
||||
const accounts = config.hosters[hoster] || [];
|
||||
if (!accounts.length) continue;
|
||||
anyAccount = true;
|
||||
const h = document.createElement('h3');
|
||||
h.style.margin = '12px 0 6px';
|
||||
h.textContent = hosterLabel(hoster);
|
||||
list.appendChild(h);
|
||||
accounts.forEach(function (a, idx) {
|
||||
list.appendChild(buildAccountCard(hoster, a, idx));
|
||||
});
|
||||
}
|
||||
if (!anyAccount) {
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Keine Accounts. Klicke oben "+ Account hinzufügen".';
|
||||
list.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
function buildAccountCard(hoster, a, idx) {
|
||||
const disabled = a.enabled === false;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'account-card' + (disabled ? ' disabled' : '');
|
||||
card.dataset.hoster = hoster;
|
||||
card.dataset.id = a.id;
|
||||
const info = document.createElement('div');
|
||||
info.className = 'account-info';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'title';
|
||||
title.textContent = accountLabel(hoster, a);
|
||||
const prio = document.createElement('span');
|
||||
prio.style.color = 'var(--text-dim)';
|
||||
prio.style.fontSize = '11px';
|
||||
prio.style.marginLeft = '6px';
|
||||
prio.textContent = '#' + (idx + 1);
|
||||
title.appendChild(prio);
|
||||
const sub = document.createElement('div');
|
||||
sub.className = 'sub';
|
||||
sub.textContent = a.auth_type === 'api' ? 'API Key' : ('Login ' + (a.username || ''));
|
||||
info.appendChild(title);
|
||||
info.appendChild(sub);
|
||||
const status = document.createElement('span');
|
||||
status.className = 'account-status';
|
||||
status.textContent = disabled ? 'Deaktiviert' : 'Aktiv';
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.className = 'btn';
|
||||
toggleBtn.dataset.act = 'toggle';
|
||||
toggleBtn.textContent = disabled ? 'Aktivieren' : 'Deaktivieren';
|
||||
toggleBtn.addEventListener('click', onAccountAction);
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.className = 'btn';
|
||||
delBtn.dataset.act = 'delete';
|
||||
delBtn.textContent = 'Löschen';
|
||||
delBtn.addEventListener('click', onAccountAction);
|
||||
card.appendChild(info);
|
||||
card.appendChild(status);
|
||||
card.appendChild(toggleBtn);
|
||||
card.appendChild(delBtn);
|
||||
return card;
|
||||
}
|
||||
|
||||
async function onAccountAction(e) {
|
||||
const card = e.currentTarget.closest('.account-card');
|
||||
const hoster = card.dataset.hoster;
|
||||
const id = card.dataset.id;
|
||||
const act = e.currentTarget.dataset.act;
|
||||
if (!config.hosters[hoster]) return;
|
||||
if (act === 'toggle') {
|
||||
const acc = config.hosters[hoster].find(function (a) { return a.id === id; });
|
||||
if (acc) acc.enabled = !acc.enabled;
|
||||
} else if (act === 'delete') {
|
||||
if (!confirm('Account wirklich löschen?')) return;
|
||||
config.hosters[hoster] = config.hosters[hoster].filter(function (a) { return a.id !== id; });
|
||||
}
|
||||
await invoke('save_config', { config: config });
|
||||
renderAccounts();
|
||||
renderHosterCheckboxes();
|
||||
}
|
||||
|
||||
$('addAccountBtn').addEventListener('click', function () {
|
||||
$('accUsername').value = '';
|
||||
$('accPassword').value = '';
|
||||
$('accApiKey').value = '';
|
||||
onAccHosterChange();
|
||||
$('accountModal').style.display = 'flex';
|
||||
});
|
||||
|
||||
function onAccHosterChange() {
|
||||
const h = $('accHoster').value;
|
||||
const needsLogin = h === 'vidmoly.me' || h === 'doodstream.com' || h === 'voe.sx';
|
||||
$('accLoginRow').style.display = needsLogin ? '' : 'none';
|
||||
$('accPasswordRow').style.display = needsLogin ? '' : 'none';
|
||||
$('accApiKeyRow').style.display = needsLogin ? 'none' : '';
|
||||
}
|
||||
$('accHoster').addEventListener('change', onAccHosterChange);
|
||||
$('accCancelBtn').addEventListener('click', function () { $('accountModal').style.display = 'none'; });
|
||||
|
||||
$('accSaveBtn').addEventListener('click', async function () {
|
||||
const hoster = $('accHoster').value;
|
||||
const needsLogin = $('accLoginRow').style.display !== 'none';
|
||||
const acc = {
|
||||
id: hoster + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6),
|
||||
enabled: true,
|
||||
auth_type: needsLogin ? 'login' : 'api',
|
||||
username: needsLogin ? $('accUsername').value.trim() : '',
|
||||
password: needsLogin ? $('accPassword').value : '',
|
||||
api_key: needsLogin ? '' : $('accApiKey').value.trim(),
|
||||
label: null,
|
||||
};
|
||||
if (!config.hosters[hoster]) config.hosters[hoster] = [];
|
||||
config.hosters[hoster].push(acc);
|
||||
await invoke('save_config', { config: config });
|
||||
$('accountModal').style.display = 'none';
|
||||
renderAccounts();
|
||||
renderHosterCheckboxes();
|
||||
showToast('Account gespeichert');
|
||||
});
|
||||
|
||||
// Hoster selection
|
||||
function renderHosterCheckboxes() {
|
||||
const container = $('hosterCheckboxes');
|
||||
while (container.firstChild) container.removeChild(container.firstChild);
|
||||
const available = ['clouddrop.cc', 'byse.sx', 'vidmoly.me', 'doodstream.com', 'voe.sx']
|
||||
.filter(function (h) {
|
||||
return config && config.hosters[h] && config.hosters[h].some(function (a) { return a.enabled !== false && hasCreds(h, a); });
|
||||
});
|
||||
if (!available.length) {
|
||||
const s = document.createElement('span');
|
||||
s.style.color = 'var(--text-dim)';
|
||||
s.textContent = 'Keine Accounts mit Credentials';
|
||||
container.appendChild(s);
|
||||
return;
|
||||
}
|
||||
available.forEach(function (h) {
|
||||
const lbl = document.createElement('label');
|
||||
lbl.className = 'hoster-checkbox' + (selectedHosters.indexOf(h) >= 0 ? ' checked' : '');
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = h;
|
||||
cb.checked = selectedHosters.indexOf(h) >= 0;
|
||||
cb.addEventListener('change', function () {
|
||||
selectedHosters = Array.from(container.querySelectorAll('input:checked')).map(function (c) { return c.value; });
|
||||
renderHosterCheckboxes();
|
||||
updateStartBtn();
|
||||
});
|
||||
lbl.appendChild(cb);
|
||||
lbl.appendChild(document.createTextNode(' ' + hosterLabel(h)));
|
||||
container.appendChild(lbl);
|
||||
});
|
||||
}
|
||||
|
||||
function updateStartBtn() {
|
||||
$('startBtn').disabled = uploading || !selectedFiles.length || !selectedHosters.length;
|
||||
}
|
||||
|
||||
// File picker
|
||||
$('pickFilesBtn').addEventListener('click', async function () {
|
||||
const picked = await dialog.open({ multiple: true, directory: false });
|
||||
if (!picked) return;
|
||||
const arr = Array.isArray(picked) ? picked : [picked];
|
||||
arr.forEach(function (p) {
|
||||
if (!selectedFiles.find(function (f) { return f.path === p; })) {
|
||||
selectedFiles.push({ path: p, name: p.split(/[\\/]/).pop() });
|
||||
}
|
||||
});
|
||||
renderQueuePreview();
|
||||
updateStartBtn();
|
||||
});
|
||||
|
||||
function renderQueuePreview() {
|
||||
const tbody = $('queueBody');
|
||||
while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
|
||||
if (!selectedFiles.length && !queueJobs.length) {
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.colSpan = 6;
|
||||
td.style.color = 'var(--text-dim)';
|
||||
td.style.textAlign = 'center';
|
||||
td.style.padding = '20px';
|
||||
td.textContent = 'Keine Dateien';
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
return;
|
||||
}
|
||||
if (queueJobs.length) {
|
||||
queueJobs.forEach(function (j) { tbody.appendChild(buildQueueRow(j)); });
|
||||
} else {
|
||||
selectedFiles.forEach(function (f) {
|
||||
selectedHosters.forEach(function (h) {
|
||||
tbody.appendChild(buildPreviewRow(f, h));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueueRow(j) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'queue-row status-' + j.status;
|
||||
tr.dataset.id = j.id;
|
||||
const pct = Math.round((j.progress || 0) * 100);
|
||||
const link = j.result ? (j.result.download_url || '') : '';
|
||||
|
||||
const td1 = document.createElement('td'); td1.textContent = j.fileName || j.file_name;
|
||||
const td2 = document.createElement('td'); td2.textContent = hosterLabel(j.hoster);
|
||||
const td3 = document.createElement('td'); td3.textContent = statusLabel(j.status);
|
||||
const td4 = document.createElement('td');
|
||||
const bg = document.createElement('span'); bg.className = 'progress-bar-bg';
|
||||
const fill = document.createElement('span'); fill.className = 'progress-bar-fill status-' + j.status; fill.style.width = pct + '%';
|
||||
bg.appendChild(fill);
|
||||
const pctSpan = document.createElement('span'); pctSpan.className = 'progress-pct'; pctSpan.textContent = pct + '%';
|
||||
td4.appendChild(bg); td4.appendChild(pctSpan);
|
||||
const td5 = document.createElement('td');
|
||||
td5.textContent = j.speedKbs ? (j.speedKbs > 1024 ? (j.speedKbs/1024).toFixed(1) + ' MB/s' : j.speedKbs + ' KB/s') : '';
|
||||
const td6 = document.createElement('td');
|
||||
if (link) {
|
||||
const a = document.createElement('a'); a.href = link; a.target = '_blank'; a.rel = 'noreferrer'; a.textContent = link;
|
||||
td6.appendChild(a);
|
||||
}
|
||||
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4); tr.appendChild(td5); tr.appendChild(td6);
|
||||
return tr;
|
||||
}
|
||||
|
||||
function buildPreviewRow(f, h) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'queue-row status-preview';
|
||||
['name','hoster','Vorschau','','',''].forEach(function () {});
|
||||
const td1 = document.createElement('td'); td1.textContent = f.name;
|
||||
const td2 = document.createElement('td'); td2.textContent = hosterLabel(h);
|
||||
const td3 = document.createElement('td'); td3.textContent = 'Vorschau';
|
||||
const td4 = document.createElement('td');
|
||||
const td5 = document.createElement('td');
|
||||
const td6 = document.createElement('td');
|
||||
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4); tr.appendChild(td5); tr.appendChild(td6);
|
||||
return tr;
|
||||
}
|
||||
|
||||
// Start batch
|
||||
$('startBtn').addEventListener('click', async function () {
|
||||
if (!selectedFiles.length || !selectedHosters.length) return;
|
||||
const jobs = [];
|
||||
queueJobs = [];
|
||||
jobById.clear();
|
||||
selectedFiles.forEach(function (file) {
|
||||
selectedHosters.forEach(function (hoster) {
|
||||
const accounts = config.hosters[hoster] || [];
|
||||
const primary = accounts.find(function (a) { return a.enabled && hasCreds(hoster, a); });
|
||||
if (!primary) return;
|
||||
const job = {
|
||||
id: uuid(),
|
||||
upload_id: uuid(),
|
||||
file: file.path,
|
||||
file_name: file.name,
|
||||
hoster: hoster,
|
||||
account_id: primary.id,
|
||||
username: primary.username || '',
|
||||
password: primary.password || '',
|
||||
api_key: primary.api_key || '',
|
||||
max_attempts: 3,
|
||||
};
|
||||
jobs.push(job);
|
||||
const jobCopy = Object.assign({}, job, { fileName: job.file_name, status: 'queued', progress: 0 });
|
||||
queueJobs.push(jobCopy);
|
||||
jobById.set(job.id, jobCopy);
|
||||
});
|
||||
});
|
||||
if (!jobs.length) { showToast('Keine gültigen Jobs — prüfe Accounts'); return; }
|
||||
|
||||
uploading = true;
|
||||
updateStartBtn();
|
||||
$('cancelBtn').disabled = false;
|
||||
renderQueuePreview();
|
||||
|
||||
try {
|
||||
await invoke('start_batch', {
|
||||
payload: {
|
||||
jobs: jobs,
|
||||
hoster_settings: config.hosterSettings || {},
|
||||
global_settings: config.globalSettings || {},
|
||||
accounts: config.hosters,
|
||||
},
|
||||
});
|
||||
showToast('Batch abgeschlossen');
|
||||
} catch (err) {
|
||||
showToast('Batch-Fehler: ' + err);
|
||||
} finally {
|
||||
uploading = false;
|
||||
updateStartBtn();
|
||||
$('cancelBtn').disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
$('cancelBtn').addEventListener('click', async function () {
|
||||
await invoke('cancel_batch');
|
||||
showToast('Abgebrochen');
|
||||
});
|
||||
|
||||
// Events
|
||||
listen('upload-progress', function (ev) {
|
||||
const p = ev.payload;
|
||||
const job = jobById.get(p.jobId);
|
||||
if (!job) return;
|
||||
job.status = p.status;
|
||||
job.progress = p.progress;
|
||||
job.speedKbs = p.speedKbs;
|
||||
job.bytesUploaded = p.bytesUploaded;
|
||||
job.bytesTotal = p.bytesTotal;
|
||||
if (p.result) job.result = p.result;
|
||||
if (p.error) job.error = p.error;
|
||||
renderQueuePreview();
|
||||
updateQueueStats();
|
||||
});
|
||||
|
||||
listen('upload-stats', function (ev) {
|
||||
const s = ev.payload;
|
||||
$('queueStats').textContent =
|
||||
'Aktiv: ' + s.activeJobs + ' | Wartet: ' + s.pendingJobs + ' | ' +
|
||||
(s.globalSpeedKbs/1024).toFixed(1) + ' MB/s';
|
||||
});
|
||||
|
||||
listen('upload-batch-done', function (ev) {
|
||||
const s = ev.payload;
|
||||
uploading = false;
|
||||
updateStartBtn();
|
||||
$('cancelBtn').disabled = true;
|
||||
showToast('Batch fertig: ' + s.succeeded + '/' + s.total + ' erfolgreich');
|
||||
});
|
||||
|
||||
listen('account-rotation-log', function (ev) {
|
||||
const p = ev.payload;
|
||||
const el = $('rotLog');
|
||||
const extras = Object.keys(p).filter(function (k) { return k !== 'ts' && k !== 'event'; })
|
||||
.map(function (k) { return k + '=' + (typeof p[k] === 'string' ? p[k] : JSON.stringify(p[k])); }).join(' ');
|
||||
const line = '[' + new Date(p.ts).toISOString() + '] [' + p.event + '] ' + extras;
|
||||
el.textContent = line + '\n' + el.textContent;
|
||||
if (p.event === 'rotate') showToast(hosterLabel(p.hoster) + ': Account-Wechsel → Fallback');
|
||||
if (p.event === 'final-error') showToast(hosterLabel(p.hoster) + ': Alle Accounts ausgeschöpft');
|
||||
});
|
||||
|
||||
listen('account-switched', function (ev) {
|
||||
const p = ev.payload;
|
||||
showToast(hosterLabel(p.hoster) + ': ' + String(p.toAccountId).slice(-6) + ' aktiv');
|
||||
});
|
||||
|
||||
function updateQueueStats() {
|
||||
const total = queueJobs.length;
|
||||
const done = queueJobs.filter(function (j) { return j.status === 'done'; }).length;
|
||||
const err = queueJobs.filter(function (j) { return j.status === 'error'; }).length;
|
||||
$('queueStats').textContent = done + '/' + total + ' fertig' + (err ? (' • ' + err + ' Fehler') : '');
|
||||
}
|
||||
|
||||
// Settings
|
||||
function renderSettings() {
|
||||
if (!config) return;
|
||||
$('globalSpeed').value = config.globalSettings.globalMaxSpeedKbs || 0;
|
||||
$('globalParallel').value = config.globalSettings.parallelUploadCount || 0;
|
||||
}
|
||||
|
||||
$('saveSettingsBtn').addEventListener('click', async function () {
|
||||
config.globalSettings.globalMaxSpeedKbs = parseInt($('globalSpeed').value) || 0;
|
||||
config.globalSettings.parallelUploadCount = parseInt($('globalParallel').value) || 0;
|
||||
await invoke('save_config', { config: config });
|
||||
showToast('Gespeichert');
|
||||
});
|
||||
|
||||
// Log
|
||||
async function refreshLog() {
|
||||
try {
|
||||
const content = await invoke('read_rotation_log');
|
||||
$('rotLog').textContent = content || '(noch keine Rotation-Events)';
|
||||
} catch (e) { $('rotLog').textContent = 'Fehler: ' + e; }
|
||||
}
|
||||
$('refreshLogBtn').addEventListener('click', refreshLog);
|
||||
$('openLogFolderBtn').addEventListener('click', function () { invoke('open_log_folder').catch(function () {}); });
|
||||
|
||||
// Init
|
||||
loadConfig().catch(function (err) {
|
||||
const div = document.createElement('div');
|
||||
div.style.padding = '20px';
|
||||
div.style.color = 'var(--danger)';
|
||||
div.textContent = 'Config-Fehler: ' + err;
|
||||
document.body.insertBefore(div, document.body.firstChild);
|
||||
});
|
||||
renderQueuePreview();
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Multi-Hoster-Upload 2.0</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<div class="app-title">Multi-Hoster-Upload <span class="ver">v2.0</span></div>
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-view="upload">Upload</button>
|
||||
<button class="tab" data-view="accounts">Accounts</button>
|
||||
<button class="tab" data-view="settings">Einstellungen</button>
|
||||
<button class="tab" data-view="log">Rotation-Log</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="view active" id="upload-view">
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<div class="drop-hint">Dateien hierher ziehen oder Button klicken</div>
|
||||
<button class="btn btn-primary" id="pickFilesBtn">+ Dateien wählen</button>
|
||||
</div>
|
||||
|
||||
<div class="hoster-picker">
|
||||
<label>Upload zu:</label>
|
||||
<div id="hosterCheckboxes"></div>
|
||||
</div>
|
||||
|
||||
<div class="queue-shell">
|
||||
<div class="queue-toolbar">
|
||||
<button class="btn btn-primary" id="startBtn" disabled>▶ Upload starten</button>
|
||||
<button class="btn" id="cancelBtn" disabled>✕ Abbrechen</button>
|
||||
<span class="queue-stats" id="queueStats">—</span>
|
||||
</div>
|
||||
<table class="queue-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datei</th>
|
||||
<th>Hoster</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Speed</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="accounts-view">
|
||||
<div class="actions-bar">
|
||||
<button class="btn btn-primary" id="addAccountBtn">+ Account hinzufügen</button>
|
||||
</div>
|
||||
<div id="accountsList" class="accounts-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="settings-view">
|
||||
<div class="settings">
|
||||
<h2>Globale Einstellungen</h2>
|
||||
<div class="setting-row">
|
||||
<label>Gesamt-Upload-Limit (KB/s, 0 = unlimitiert)</label>
|
||||
<input type="number" id="globalSpeed" min="0" value="0" />
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>Parallele Uploads über alle Hoster (0 = nur pro Hoster)</label>
|
||||
<input type="number" id="globalParallel" min="0" max="100" value="0" />
|
||||
</div>
|
||||
<button class="btn btn-primary" id="saveSettingsBtn">Speichern</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="log-view">
|
||||
<div class="actions-bar">
|
||||
<button class="btn" id="refreshLogBtn">Aktualisieren</button>
|
||||
<button class="btn" id="openLogFolderBtn">Log-Ordner öffnen</button>
|
||||
</div>
|
||||
<pre id="rotLog" class="log-pre"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Account modal -->
|
||||
<div class="modal-overlay" id="accountModal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<h3>Account hinzufügen</h3>
|
||||
<div class="setting-row">
|
||||
<label>Hoster</label>
|
||||
<select id="accHoster">
|
||||
<option value="clouddrop.cc">Clouddrop (API)</option>
|
||||
<option value="byse.sx">Byse (API)</option>
|
||||
<option value="vidmoly.me">Vidmoly (Login)</option>
|
||||
<option value="doodstream.com">Doodstream (nicht in 2.0 POC)</option>
|
||||
<option value="voe.sx">VOE (nicht in 2.0 POC)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row" id="accLoginRow" style="display:none">
|
||||
<label>Username</label>
|
||||
<input type="text" id="accUsername" />
|
||||
</div>
|
||||
<div class="setting-row" id="accPasswordRow" style="display:none">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="accPassword" />
|
||||
</div>
|
||||
<div class="setting-row" id="accApiKeyRow">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="accApiKey" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" id="accCancelBtn">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="accSaveBtn">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg: #16181c;
|
||||
--bg-alt: #1c1f24;
|
||||
--bg-hover: #24282e;
|
||||
--border: #2d3139;
|
||||
--text: #e6e8eb;
|
||||
--text-dim: #9aa3ae;
|
||||
--accent: #4aa3ff;
|
||||
--accent-hover: #6ab5ff;
|
||||
--danger: #ff5a5f;
|
||||
--success: #3ece78;
|
||||
--warn: #ffbf4a;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13px/1.5 "Segoe UI", system-ui, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-alt);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.app-title .ver {
|
||||
color: var(--accent);
|
||||
font-weight: normal;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
border: none;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tab:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.tab.active { background: var(--accent); color: #fff; }
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.view.active { display: block; }
|
||||
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.drop-zone.hover { border-color: var(--accent); background: rgba(74, 163, 255, 0.05); }
|
||||
|
||||
.drop-hint {
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hoster-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hoster-picker label { font-weight: 500; }
|
||||
|
||||
.hoster-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 6px;
|
||||
background: var(--bg-alt);
|
||||
}
|
||||
|
||||
.hoster-checkbox input { margin: 0; }
|
||||
.hoster-checkbox.checked { border-color: var(--accent); background: rgba(74, 163, 255, 0.12); }
|
||||
|
||||
.queue-shell {
|
||||
background: var(--bg-alt);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.queue-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.queue-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.queue-table th, .queue-table td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.queue-table th {
|
||||
background: var(--bg);
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.queue-row.status-done td { color: var(--success); }
|
||||
.queue-row.status-error td { color: var(--danger); }
|
||||
.queue-row.status-uploading td { color: var(--accent); }
|
||||
|
||||
.progress-bar-bg {
|
||||
width: 120px;
|
||||
height: 10px;
|
||||
background: var(--bg);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
transition: width 0.2s ease-out;
|
||||
}
|
||||
.progress-bar-fill.status-done { background: var(--success); }
|
||||
.progress-bar-fill.status-error { background: var(--danger); }
|
||||
.progress-pct { margin-left: 6px; font-size: 11px; color: var(--text-dim); }
|
||||
|
||||
.actions-bar {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.accounts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-alt);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.account-card.disabled { opacity: 0.5; }
|
||||
|
||||
.account-info {
|
||||
flex: 1;
|
||||
}
|
||||
.account-info .title { font-weight: 600; }
|
||||
.account-info .sub { color: var(--text-dim); font-size: 12px; }
|
||||
|
||||
.account-status {
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
.account-status.ok { background: rgba(62, 206, 120, 0.2); color: var(--success); }
|
||||
.account-status.error { background: rgba(255, 90, 95, 0.2); color: var(--danger); }
|
||||
|
||||
.settings {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.setting-row label {
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.setting-row input, .setting-row select {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.setting-row input:focus, .setting-row select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: var(--bg-alt);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: 7px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.btn:hover { background: var(--bg-hover); }
|
||||
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: var(--bg-alt);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
width: min(420px, 92vw);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-card h3 { margin-top: 0; }
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.log-pre {
|
||||
background: var(--bg-alt);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
font-family: "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
white-space: pre-wrap;
|
||||
max-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(40px);
|
||||
background: var(--bg-alt);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
z-index: 200;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
Reference in New Issue
Block a user