release: Multi-Hoster-Upload v2.0.2

This commit is contained in:
Sucukdeluxe
2026-08-07 19:35:33 +02:00
parent 376f47b960
commit 6cdccf71f2
6 changed files with 251 additions and 108 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.0.1", "version": "2.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.0.1", "version": "2.0.2",
"dependencies": { "dependencies": {
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"undici": "^7.29.0", "undici": "^7.29.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.0.1", "version": "2.0.2",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+79 -31
View File
@@ -353,10 +353,14 @@ function _isHistoryTabActive() {
if (!tab || tab === activeTab) return; if (!tab || tab === activeTab) return;
if (activeTab) { if (activeTab) {
activeTab.classList.remove('active'); activeTab.classList.remove('active');
activeTab.setAttribute('aria-selected', 'false');
activeTab.tabIndex = -1;
const prevView = viewsById[`${activeTab.dataset.view}-view`]; const prevView = viewsById[`${activeTab.dataset.view}-view`];
if (prevView) prevView.classList.remove('active'); if (prevView) prevView.classList.remove('active');
} }
tab.classList.add('active'); tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
tab.tabIndex = 0;
const nextView = viewsById[`${tab.dataset.view}-view`]; const nextView = viewsById[`${tab.dataset.view}-view`];
if (nextView) nextView.classList.add('active'); if (nextView) nextView.classList.add('active');
activeTab = tab; activeTab = tab;
@@ -368,6 +372,18 @@ function _isHistoryTabActive() {
const tabBar = tabs[0] && tabs[0].parentElement; const tabBar = tabs[0] && tabs[0].parentElement;
if (tabBar) { if (tabBar) {
tabBar.addEventListener('click', (e) => handle(e.target)); tabBar.addEventListener('click', (e) => handle(e.target));
tabBar.addEventListener('keydown', (e) => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
e.preventDefault();
const current = Math.max(0, tabs.indexOf(activeTab));
const next = e.key === 'Home'
? 0
: e.key === 'End'
? tabs.length - 1
: (current + (e.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
tabs[next].focus();
handle(tabs[next]);
});
} else { } else {
// Fallback: bind per-tab if somehow no common parent // Fallback: bind per-tab if somehow no common parent
tabs.forEach(t => t.addEventListener('click', () => handle(t))); tabs.forEach(t => t.addEventListener('click', () => handle(t)));
@@ -1920,9 +1936,20 @@ document.addEventListener('click', (e) => {
if (!e.target.closest('.context-menu')) hideContextMenu(); if (!e.target.closest('.context-menu')) hideContextMenu();
}); });
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
const accountModal = document.getElementById('accountModal');
if (e.key === 'Tab' && accountModal && accountModal.style.display !== 'none') {
const focusable = Array.from(accountModal.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'));
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (first && last && ((!e.shiftKey && document.activeElement === last) || (e.shiftKey && document.activeElement === first))) {
e.preventDefault();
(e.shiftKey ? last : first).focus();
}
}
if (e.key === 'Escape') { if (e.key === 'Escape') {
hideContextMenu(); hideContextMenu();
cancelHosterModal(); cancelHosterModal();
if (accountModal && accountModal.style.display !== 'none') closeAccountModal();
} }
if (e.target.closest('input, textarea, select')) return; if (e.target.closest('input, textarea, select')) return;
const activeView = document.querySelector('.view.active'); const activeView = document.querySelector('.view.active');
@@ -2951,12 +2978,12 @@ function updateStatusBar() {
const totalSize = Math.max(stats.totalSize, _sessionTotalBytes); const totalSize = Math.max(stats.totalSize, _sessionTotalBytes);
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`; document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`; document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`; document.getElementById('sbConnections').textContent = `Verbindungen ${lastUploadStats.activeJobs || 0}`;
document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`; document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`;
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`; document.getElementById('sbRemainingCount').textContent = `Verbleibend ${stats.remaining}`;
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`; document.getElementById('sbInProgressCount').textContent = `Läuft ${stats.inProgress}`;
document.getElementById('sbDoneCount').textContent = `Done: ${_sessionDoneCount}`; document.getElementById('sbDoneCount').textContent = `Fertig ${_sessionDoneCount}`;
document.getElementById('sbErrorCount').textContent = `Error: ${_sessionErrorCount}`; document.getElementById('sbErrorCount').textContent = `Fehler ${_sessionErrorCount}`;
} }
// --- Health Check --- // --- Health Check ---
@@ -3864,8 +3891,10 @@ function renderAccounts() {
if (allAccounts.length === 0) { if (allAccounts.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="accounts-empty"> <div class="accounts-empty">
<p>Keine Accounts vorhanden</p> <div class="accounts-empty-icon" aria-hidden="true">+</div>
<span class="hint">Klicke auf "Account hinzufügen", um einen Hoster einzurichten.</span> <h3>Noch keine Accounts</h3>
<p>Füge deinen ersten Hoster-Account hinzu. Die Zugangsdaten werden vor dem Speichern geprüft.</p>
<button class="btn btn-primary" type="button" data-account-empty-add>Ersten Account hinzufügen</button>
</div>`; </div>`;
if (footer) footer.style.display = 'none'; if (footer) footer.style.display = 'none';
if (!_accountListenersBound) bindAccountListeners(container); if (!_accountListenersBound) bindAccountListeners(container);
@@ -4106,6 +4135,7 @@ function bindAccountListeners(container) {
} }
const btn = e.target.closest('button'); const btn = e.target.closest('button');
if (!btn) return; if (!btn) return;
if (btn.hasAttribute('data-account-empty-add')) return openAccountModal(null);
if (btn.dataset.accountToggle) return toggleAccount(btn.dataset.accountToggle); if (btn.dataset.accountToggle) return toggleAccount(btn.dataset.accountToggle);
if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit); if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit);
if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete); if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete);
@@ -4240,25 +4270,42 @@ function getCredsFieldsHtml(authType, account, hoster) {
}; };
return ` return `
<div class="settings-row"> <div class="settings-row">
<label>${escapeHtml(fld.label)}</label> <label for="accField_username">${escapeHtml(fld.label)}</label>
<input type="${fld.inputType}" class="key-input" id="accField_username" value="${escapeAttr(account.username || '')}" placeholder="${escapeAttr(fld.placeholder)}"> <input type="${fld.inputType}" class="key-input" id="accField_username" name="username" autocomplete="username" spellcheck="false" value="${escapeAttr(account.username || '')}" placeholder="${escapeAttr(fld.placeholder)}">
</div> </div>
<div class="settings-row"> <div class="settings-row">
<label>Passwort</label> <label for="accField_password">Passwort</label>
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(account.password || '')}" placeholder="Passwort"> <input type="password" class="key-input" id="accField_password" name="password" autocomplete="current-password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button> <button class="toggle-vis" type="button" title="Passwort anzeigen" aria-label="Passwort anzeigen" aria-pressed="false">&#128065;</button>
</div>`; </div>`;
} }
// API key // API key
return ` return `
<div class="settings-row"> <div class="settings-row">
<label>API Key</label> <label for="accField_apiKey">API-Key</label>
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(account.apiKey || '')}" placeholder="API Key"> <input type="password" class="key-input" id="accField_apiKey" name="apiKey" autocomplete="off" spellcheck="false" value="${escapeAttr(account.apiKey || '')}" placeholder="API-Key">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button> <button class="toggle-vis" type="button" title="API-Key anzeigen" aria-label="API-Key anzeigen" aria-pressed="false">&#128065;</button>
</div>`; </div>`;
} }
function wireCredentialVisibilityButtons(container) {
container.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
const visible = input.type === 'password';
const fieldName = input.id === 'accField_apiKey' ? 'API-Key' : 'Passwort';
input.type = visible ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(visible));
btn.setAttribute('aria-label', `${fieldName} ${visible ? 'verbergen' : 'anzeigen'}`);
btn.title = `${fieldName} ${visible ? 'verbergen' : 'anzeigen'}`;
});
});
}
let _accountModalReturnFocus = null;
function openAccountModal(editAccountId) { function openAccountModal(editAccountId) {
_accountModalReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
editingAccountId = editAccountId || null; editingAccountId = editAccountId || null;
_resetAccountModalState(); _resetAccountModalState();
const modal = document.getElementById('accountModal'); const modal = document.getElementById('accountModal');
@@ -4298,17 +4345,17 @@ function openAccountModal(editAccountId) {
credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {}, firstOpt.value); credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {}, firstOpt.value);
} }
// Toggle visibility buttons wireCredentialVisibilityButtons(credsContainer);
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
_wireCredFieldInvalidation(); _wireCredFieldInvalidation();
modal.style.display = 'flex'; modal.style.display = 'flex';
requestAnimationFrame(() => {
const firstControl = editingAccountId
? document.getElementById('accField_label')
: hosterSelect;
if (firstControl) firstControl.focus();
});
} }
function closeAccountModal() { function closeAccountModal() {
@@ -4316,6 +4363,12 @@ function closeAccountModal() {
_hideOtpField(); _hideOtpField();
editingAccountId = null; editingAccountId = null;
_resetAccountModalState(); _resetAccountModalState();
const returnFocus = _accountModalReturnFocus;
_accountModalReturnFocus = null;
const focusTarget = returnFocus && returnFocus.isConnected
? returnFocus
: document.getElementById('addAccountBtn');
if (focusTarget) focusTarget.focus();
} }
function openDeleteAccountModal(accountId) { function openDeleteAccountModal(accountId) {
@@ -4879,7 +4932,7 @@ function renderHistoryTable(container) {
}; };
container.innerHTML = `<table class="results-table history-table"><thead><tr> container.innerHTML = `<table class="results-table history-table"><thead><tr>
${headerCell('date', 'Date')}${headerCell('filename', 'Filename')}${headerCell('host', 'Host')}${headerCell('link', 'Link')} ${headerCell('date', 'Datum')}${headerCell('filename', 'Dateiname')}${headerCell('host', 'Hoster')}${headerCell('link', 'Link')}
</tr></thead><tbody id="historyBody"></tbody></table>`; </tr></thead><tbody id="historyBody"></tbody></table>`;
if (!_historyListenersBound) { if (!_historyListenersBound) {
@@ -5136,12 +5189,7 @@ function setupListeners() {
const authType = opt ? opt.authType : 'login'; const authType = opt ? opt.authType : 'login';
const credsContainer = document.getElementById('accountCredsFields'); const credsContainer = document.getElementById('accountCredsFields');
credsContainer.innerHTML = getCredsFieldsHtml(authType, {}, e.target.value); credsContainer.innerHTML = getCredsFieldsHtml(authType, {}, e.target.value);
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => { wireCredentialVisibilityButtons(credsContainer);
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
_wireCredFieldInvalidation(); _wireCredFieldInvalidation();
}); });
@@ -5185,7 +5233,7 @@ function showUpdateBanner(info) {
function handleUpdateProgress(data) { function handleUpdateProgress(data) {
const msg = document.getElementById('updateMessage'); const msg = document.getElementById('updateMessage');
if (!msg) return; if (!msg) return;
if (data.stage === 'downloading') msg.textContent = `Downloading... ${data.percent || 0}%`; if (data.stage === 'downloading') msg.textContent = `Wird heruntergeladen… ${data.percent || 0}%`;
else if (data.stage === 'verifying') msg.textContent = 'Verifiziere...'; else if (data.stage === 'verifying') msg.textContent = 'Verifiziere...';
else if (data.stage === 'launching') msg.textContent = 'Setup wird gestartet...'; else if (data.stage === 'launching') msg.textContent = 'Setup wird gestartet...';
else if (data.stage === 'done') msg.textContent = 'Update installiert. App wird neu gestartet...'; else if (data.stage === 'done') msg.textContent = 'Update installiert. App wird neu gestartet...';
+60 -60
View File
@@ -66,11 +66,11 @@
</div> </div>
</nav> </nav>
<nav class="tab-bar"> <nav class="tab-bar" role="tablist" aria-label="Hauptbereiche">
<button class="tab active" data-view="upload">Upload</button> <button class="tab active" id="upload-tab" role="tab" aria-selected="true" aria-controls="upload-view" tabindex="0" data-view="upload">Upload</button>
<button class="tab" data-view="accounts">Accounts</button> <button class="tab" id="accounts-tab" role="tab" aria-selected="false" aria-controls="accounts-view" tabindex="-1" data-view="accounts">Accounts</button>
<button class="tab" data-view="settings">Einstellungen</button> <button class="tab" id="settings-tab" role="tab" aria-selected="false" aria-controls="settings-view" tabindex="-1" data-view="settings">Einstellungen</button>
<button class="tab" data-view="history">Verlauf</button> <button class="tab" id="history-tab" role="tab" aria-selected="false" aria-controls="history-view" tabindex="-1" data-view="history">Verlauf</button>
<span class="version-label" id="versionLabel"></span> <span class="version-label" id="versionLabel"></span>
</nav> </nav>
@@ -80,7 +80,7 @@
<button class="btn btn-sm btn-secondary" id="dismissUpdateBtn">&times;</button> <button class="btn btn-sm btn-secondary" id="dismissUpdateBtn">&times;</button>
</div> </div>
<div id="upload-view" class="view active"> <div id="upload-view" class="view active" role="tabpanel" aria-labelledby="upload-tab">
<div class="upload-toolbar"> <div class="upload-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<span class="hoster-summary" id="hosterSummary" style="display:none"></span> <span class="hoster-summary" id="hosterSummary" style="display:none"></span>
@@ -99,35 +99,35 @@
<div class="queue-shell" id="queueShell" style="display:none"> <div class="queue-shell" id="queueShell" style="display:none">
<div class="queue-command-bar" id="queueCommandBar"> <div class="queue-command-bar" id="queueCommandBar">
<button class="toolbar-btn" id="startUploadBtn" title="Start all" disabled> <button class="toolbar-btn" id="startUploadBtn" title="Alle Uploads starten" aria-label="Alle Uploads starten" disabled>
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z" fill="#4caf50"/></svg> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z" fill="#4caf50"/></svg>
</button> </button>
<button class="toolbar-btn" id="startSelectedBtn" title="Start selected" disabled> <button class="toolbar-btn" id="startSelectedBtn" title="Ausgewählte Uploads starten" aria-label="Ausgewählte Uploads starten" disabled>
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6 3l8 5-8 5z" fill="#4caf50"/><rect x="1" y="3" width="3" height="10" rx="0.5" fill="#4caf50"/></svg> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M6 3l8 5-8 5z" fill="#4caf50"/><rect x="1" y="3" width="3" height="10" rx="0.5" fill="#4caf50"/></svg>
</button> </button>
<button class="toolbar-btn" id="reuploadSelectedBtn" title="Reupload selected file"> <button class="toolbar-btn" id="reuploadSelectedBtn" title="Ausgewählte Datei erneut hochladen" aria-label="Ausgewählte Datei erneut hochladen">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 1a7 7 0 0 0-5 2.1V1H2v4h4V4H3.7A5.5 5.5 0 1 1 2.5 8H1a7 7 0 1 0 7-7z" fill="#4caf50"/></svg> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M8 1a7 7 0 0 0-5 2.1V1H2v4h4V4H3.7A5.5 5.5 0 1 1 2.5 8H1a7 7 0 1 0 7-7z" fill="#4caf50"/></svg>
</button> </button>
<button class="toolbar-btn" id="abortSelectedBtn" title="Abort selected file"> <button class="toolbar-btn" id="abortSelectedBtn" title="Ausgewählten Upload abbrechen" aria-label="Ausgewählten Upload abbrechen">
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="1" fill="#e53935"/><path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="1" fill="#e53935"/><path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>
</button> </button>
<button class="toolbar-btn" id="finishStopBtn" title="Finish Uploads in Progress and Stop"> <button class="toolbar-btn" id="finishStopBtn" title="Aktive Uploads beenden und stoppen" aria-label="Aktive Uploads beenden und stoppen">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M2 8l4 4 8-8" stroke="#4caf50" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="11" y="9" width="5" height="5" rx="1" fill="#e53935"/></svg> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M2 8l4 4 8-8" stroke="#4caf50" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="11" y="9" width="5" height="5" rx="1" fill="#e53935"/></svg>
</button> </button>
<button class="toolbar-btn toolbar-btn-danger" id="abortAllBtn" title="Abort all Downloads"> <button class="toolbar-btn toolbar-btn-danger" id="abortAllBtn" title="Alle Uploads abbrechen" aria-label="Alle Uploads abbrechen">
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="#e53935"/><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="#e53935"/><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
</button> </button>
<span class="toolbar-sep"></span> <span class="toolbar-sep"></span>
<button class="toolbar-btn" id="moveTopBtn" title="Move to the top"> <button class="toolbar-btn" id="moveTopBtn" title="Ganz nach oben" aria-label="Ganz nach oben">
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="4" y="1" width="8" height="2" rx="0.5" fill="#4caf50"/><path d="M8 5l-4 5h8z" fill="#4caf50"/><path d="M8 9l-4 5h8z" fill="#4caf50"/></svg> <svg width="16" height="16" viewBox="0 0 16 16"><rect x="4" y="1" width="8" height="2" rx="0.5" fill="#4caf50"/><path d="M8 5l-4 5h8z" fill="#4caf50"/><path d="M8 9l-4 5h8z" fill="#4caf50"/></svg>
</button> </button>
<button class="toolbar-btn" id="moveUpBtn" title="Move up"> <button class="toolbar-btn" id="moveUpBtn" title="Nach oben" aria-label="Nach oben">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2l-5 6h10z" fill="#4caf50"/><rect x="6" y="8" width="4" height="6" rx="0.5" fill="#4caf50"/></svg> <svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2l-5 6h10z" fill="#4caf50"/><rect x="6" y="8" width="4" height="6" rx="0.5" fill="#4caf50"/></svg>
</button> </button>
<button class="toolbar-btn" id="moveDownBtn" title="Move down"> <button class="toolbar-btn" id="moveDownBtn" title="Nach unten" aria-label="Nach unten">
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="6" y="2" width="4" height="6" rx="0.5" fill="#4caf50"/><path d="M8 14l-5-6h10z" fill="#4caf50"/></svg> <svg width="16" height="16" viewBox="0 0 16 16"><rect x="6" y="2" width="4" height="6" rx="0.5" fill="#4caf50"/><path d="M8 14l-5-6h10z" fill="#4caf50"/></svg>
</button> </button>
<button class="toolbar-btn" id="moveBottomBtn" title="Move to the bottom"> <button class="toolbar-btn" id="moveBottomBtn" title="Ganz nach unten" aria-label="Ganz nach unten">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 7l-4-5h8z" fill="#4caf50"/><path d="M8 11l-4-5h8z" fill="#4caf50"/><rect x="4" y="13" width="8" height="2" rx="0.5" fill="#4caf50"/></svg> <svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 7l-4-5h8z" fill="#4caf50"/><path d="M8 11l-4-5h8z" fill="#4caf50"/><rect x="4" y="13" width="8" height="2" rx="0.5" fill="#4caf50"/></svg>
</button> </button>
</div> </div>
@@ -136,14 +136,14 @@
<table class="queue-table" id="queueTable"> <table class="queue-table" id="queueTable">
<thead> <thead>
<tr> <tr>
<th class="col-filename sortable" data-col="filename" data-sort="filename">Filename<span class="col-resizer"></span></th> <th class="col-filename sortable" data-col="filename" data-sort="filename">Dateiname<span class="col-resizer"></span></th>
<th class="col-size sortable" data-col="size" data-sort="size">Uploaded / Size<span class="col-resizer"></span></th> <th class="col-size sortable" data-col="size" data-sort="size">Hochgeladen / Größe<span class="col-resizer"></span></th>
<th class="col-host sortable" data-col="host" data-sort="host">Host<span class="col-resizer"></span></th> <th class="col-host sortable" data-col="host" data-sort="host">Hoster<span class="col-resizer"></span></th>
<th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th> <th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th>
<th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th> <th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th>
<th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th> <th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th>
<th class="col-speed sortable" data-col="speed" data-sort="speed">Speed<span class="col-resizer"></span></th> <th class="col-speed sortable" data-col="speed" data-sort="speed">Geschwindigkeit<span class="col-resizer"></span></th>
<th class="col-progress sortable" data-col="progress" data-sort="progress">Progress</th> <th class="col-progress sortable" data-col="progress" data-sort="progress">Fortschritt</th>
</tr> </tr>
</thead> </thead>
<tbody id="queueBody"></tbody> <tbody id="queueBody"></tbody>
@@ -168,8 +168,8 @@
<div class="recent-files-panel" id="recentFilesPanel"> <div class="recent-files-panel" id="recentFilesPanel">
<div class="recent-files-header"> <div class="recent-files-header">
<div class="recent-tabs"> <div class="recent-tabs">
<button class="recent-tab active" data-panel="filesTab">Files</button> <button class="recent-tab active" data-panel="filesTab">Dateien</button>
<button class="recent-tab" data-panel="statsTab">Stats</button> <button class="recent-tab" data-panel="statsTab">Statistik</button>
</div> </div>
<span class="recent-files-hint" id="recentFilesHint">Zuletzt erzeugte Upload-Links</span> <span class="recent-files-hint" id="recentFilesHint">Zuletzt erzeugte Upload-Links</span>
<button class="btn btn-xs btn-secondary" id="exportRecentFilesBtn" title="Alle Zeilen als Datei exportieren (Zeit, Hoster, Link, Dateiname)">Exportieren</button> <button class="btn btn-xs btn-secondary" id="exportRecentFilesBtn" title="Alle Zeilen als Datei exportieren (Zeit, Hoster, Link, Dateiname)">Exportieren</button>
@@ -181,8 +181,8 @@
<thead id="recentFilesHead"> <thead id="recentFilesHead">
<tr> <tr>
<th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator"></span></th> <th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator"></span></th>
<th class="col-filename sortable" data-recent-sort="filename">Filename<span class="sort-indicator"></span></th> <th class="col-filename sortable" data-recent-sort="filename">Dateiname<span class="sort-indicator"></span></th>
<th class="col-host sortable" data-recent-sort="host">Host<span class="sort-indicator"></span></th> <th class="col-host sortable" data-recent-sort="host">Hoster<span class="sort-indicator"></span></th>
<th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator"></span></th> <th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator"></span></th>
</tr> </tr>
</thead> </thead>
@@ -193,24 +193,24 @@
<div class="recent-tab-body" id="statsTab"> <div class="recent-tab-body" id="statsTab">
<div class="stats-grid"> <div class="stats-grid">
<div class="stats-col"> <div class="stats-col">
<h4>Files in queue (count)</h4> <h4>Dateien in der Warteschlange</h4>
<div class="stats-row"><span>total:</span><span id="statQueueTotal">0</span></div> <div class="stats-row"><span>Gesamt:</span><span id="statQueueTotal">0</span></div>
<div class="stats-row"><span>done:</span><span id="statQueueDone">0</span></div> <div class="stats-row"><span>Fertig:</span><span id="statQueueDone">0</span></div>
<div class="stats-row"><span>remaining:</span><span id="statQueueRemaining">0</span></div> <div class="stats-row"><span>Verbleibend:</span><span id="statQueueRemaining">0</span></div>
<div class="stats-row"><span>in progress:</span><span id="statQueueInProgress">0</span></div> <div class="stats-row"><span>Läuft:</span><span id="statQueueInProgress">0</span></div>
<div class="stats-row"><span>error:</span><span id="statQueueError">0</span></div> <div class="stats-row"><span>Fehler:</span><span id="statQueueError">0</span></div>
</div> </div>
<div class="stats-col"> <div class="stats-col">
<h4>File size in queue</h4> <h4>Dateigröße in der Warteschlange</h4>
<div class="stats-row"><span>total:</span><span id="statSizeTotal">0 B</span></div> <div class="stats-row"><span>Gesamt:</span><span id="statSizeTotal">0 B</span></div>
<div class="stats-row"><span>remaining:</span><span id="statSizeRemaining">0 B</span></div> <div class="stats-row"><span>Verbleibend:</span><span id="statSizeRemaining">0 B</span></div>
</div> </div>
<div class="stats-col"> <div class="stats-col">
<h4>Session</h4> <h4>Sitzung</h4>
<div class="stats-row"><span>Upload speed:</span><span id="statSpeed">0 B/s</span></div> <div class="stats-row"><span>Upload-Geschwindigkeit:</span><span id="statSpeed">0 B/s</span></div>
<div class="stats-row"><span>Remaining time:</span><span id="statEta">--:--</span></div> <div class="stats-row"><span>Restzeit:</span><span id="statEta">--:--</span></div>
<div class="stats-row"><span>Run time:</span><span id="statRunTime">00:00:00</span></div> <div class="stats-row"><span>Laufzeit:</span><span id="statRunTime">00:00:00</span></div>
<div class="stats-row"><span>Uploaded (this run):</span><span id="statSessionBytes">0 B</span></div> <div class="stats-row"><span>In diesem Lauf hochgeladen:</span><span id="statSessionBytes">0 B</span></div>
</div> </div>
</div> </div>
</div> </div>
@@ -219,7 +219,7 @@
</div> </div>
</div> </div>
<div id="accounts-view" class="view"> <div id="accounts-view" class="view" role="tabpanel" aria-labelledby="accounts-tab">
<div class="accounts-container"> <div class="accounts-container">
<div class="accounts-header"> <div class="accounts-header">
<div> <div>
@@ -232,7 +232,7 @@
<input type="checkbox" id="autoHealthCheckToggle" checked> <input type="checkbox" id="autoHealthCheckToggle" checked>
<span>Auto-Check vor Upload</span> <span>Auto-Check vor Upload</span>
</label> </label>
<button class="btn btn-primary" id="addAccountBtn">+ Account hinzufügen</button> <button class="btn btn-primary" id="addAccountBtn">Account hinzufügen</button>
</div> </div>
</div> </div>
<div class="health-check-results account-health-results" id="healthCheckResults"></div> <div class="health-check-results account-health-results" id="healthCheckResults"></div>
@@ -244,7 +244,7 @@
</div> </div>
<div class="modal-overlay" id="accountModal" style="display:none"> <div class="modal-overlay" id="accountModal" style="display:none">
<div class="modal-card"> <div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="accountModalTitle" aria-describedby="accountModalSubtitle">
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h3 id="accountModalTitle">Account hinzufügen</h3> <h3 id="accountModalTitle">Account hinzufügen</h3>
@@ -254,15 +254,15 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="settings-row" id="accountHosterRow"> <div class="settings-row" id="accountHosterRow">
<label>Hoster</label> <label for="accountHosterSelect">Hoster</label>
<select class="key-input" id="accountHosterSelect" style="max-width:300px"></select> <select class="key-input" id="accountHosterSelect" name="hoster" autocomplete="off" style="max-width:300px"></select>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<label>Label (optional)</label> <label for="accField_label">Label (optional)</label>
<input type="text" class="key-input" id="accField_label" placeholder="z.B. Hauptaccount, Premium, Kunde XY" maxlength="60"> <input type="text" class="key-input" id="accField_label" name="accountLabel" autocomplete="off" placeholder="z. B. Hauptaccount, Premium, Kunde XY" maxlength="60">
</div> </div>
<div id="accountCredsFields"></div> <div id="accountCredsFields"></div>
<div class="account-modal-status" id="accountModalStatus"></div> <div class="account-modal-status" id="accountModalStatus" role="status" aria-live="polite"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" id="cancelAccountModalBtn">Abbrechen</button> <button class="btn btn-secondary" id="cancelAccountModalBtn">Abbrechen</button>
@@ -303,7 +303,7 @@
</div> </div>
</div> </div>
<div id="settings-view" class="view"> <div id="settings-view" class="view" role="tabpanel" aria-labelledby="settings-tab">
<div class="settings-container"> <div class="settings-container">
<h2>Upload-Einstellungen</h2> <h2>Upload-Einstellungen</h2>
<p class="settings-hint">Hoster-Einstellungen erscheinen erst, sobald ein Account hinterlegt ist. Änderungen werden automatisch gespeichert.</p> <p class="settings-hint">Hoster-Einstellungen erscheinen erst, sobald ein Account hinterlegt ist. Änderungen werden automatisch gespeichert.</p>
@@ -315,7 +315,7 @@
</div> </div>
</div> </div>
<div id="history-view" class="view"> <div id="history-view" class="view" role="tabpanel" aria-labelledby="history-tab">
<div class="history-container"> <div class="history-container">
<div class="history-header"> <div class="history-header">
<h2>Upload-Verlauf</h2> <h2>Upload-Verlauf</h2>
@@ -368,17 +368,17 @@
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-eta" id="sbEta">ETA --:--</span> <span class="sb-eta" id="sbEta">ETA --:--</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-connections" id="sbConnections">Aktive Verbindungen 0</span> <span class="sb-connections" id="sbConnections">Verbindungen 0</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-queue-count" id="sbQueueCount">Gesamt 0</span> <span class="sb-queue-count" id="sbQueueCount">Gesamt 0</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-remaining-count" id="sbRemainingCount">Remaining 0</span> <span class="sb-remaining-count" id="sbRemainingCount">Verbleibend 0</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-progress-count" id="sbInProgressCount">In Progress 0</span> <span class="sb-progress-count" id="sbInProgressCount">Läuft 0</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-done-count" id="sbDoneCount">Done 0</span> <span class="sb-done-count" id="sbDoneCount">Fertig 0</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>
<span class="sb-error-count" id="sbErrorCount">Error 0</span> <span class="sb-error-count" id="sbErrorCount">Fehler 0</span>
</div> </div>
<div class="copy-toast" id="copyToast"></div> <div class="copy-toast" id="copyToast"></div>
+58 -11
View File
@@ -1,4 +1,5 @@
:root { :root {
color-scheme: dark;
--bg-primary: #16181c; --bg-primary: #16181c;
--bg-secondary: #20242b; --bg-secondary: #20242b;
--bg-card: #262b33; --bg-card: #262b33;
@@ -8,7 +9,7 @@
--border-hover: rgba(255, 255, 255, 0.18); --border-hover: rgba(255, 255, 255, 0.18);
--text: #edf1f7; --text: #edf1f7;
--text-muted: #9ea7b3; --text-muted: #9ea7b3;
--text-dim: #727b88; --text-dim: #8490a0;
--accent: #3ea7ff; --accent: #3ea7ff;
--accent-end: #65d8ff; --accent-end: #65d8ff;
--success: #43c788; --success: #43c788;
@@ -216,7 +217,7 @@ body {
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: all 0.2s; transition: color 0.2s, background-color 0.2s, border-color 0.2s;
} }
.tab:hover { color: var(--text); } .tab:hover { color: var(--text); }
.tab.active { color: var(--text); border-bottom-color: var(--accent); background: rgba(255, 255, 255, 0.03); } .tab.active { color: var(--text); border-bottom-color: var(--accent); background: rgba(255, 255, 255, 0.03); }
@@ -256,7 +257,7 @@ body {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
letter-spacing: 0.02em; letter-spacing: 0.02em;
transition: all 0.2s; transition: color 0.2s, background-color 0.2s, border-color 0.2s, filter 0.2s, transform 0.2s;
} }
.btn-xs { padding: 4px 10px; font-size: 11px; border-radius: 4px; } .btn-xs { padding: 4px 10px; font-size: 11px; border-radius: 4px; }
.btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 5px; } .btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 5px; }
@@ -319,7 +320,7 @@ body {
border: 1px dashed rgba(126, 220, 255, 0.28); border: 1px dashed rgba(126, 220, 255, 0.28);
border-radius: 18px; border-radius: 18px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: color 0.3s, background-color 0.3s, border-color 0.3s;
color: var(--text-muted); color: var(--text-muted);
min-height: 200px; min-height: 200px;
background: background:
@@ -359,7 +360,7 @@ body {
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
color: var(--text-muted); color: var(--text-muted);
transition: all 0.15s; transition: color 0.15s, background-color 0.15s, border-color 0.15s, transform 0.15s;
} }
.toolbar-btn:hover { .toolbar-btn:hover {
background: rgba(255,255,255,0.1); background: rgba(255,255,255,0.1);
@@ -571,7 +572,7 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
border-bottom: none; border-bottom: none;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: color 0.15s, background-color 0.15s, border-color 0.15s;
} }
.recent-tab:first-child { border-radius: 4px 0 0 0; } .recent-tab:first-child { border-radius: 4px 0 0 0; }
.recent-tab:last-child { border-radius: 0 4px 0 0; } .recent-tab:last-child { border-radius: 0 4px 0 0; }
@@ -677,6 +678,7 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
background: rgba(5, 7, 16, 0.72); background: rgba(5, 7, 16, 0.72);
z-index: 2500; z-index: 2500;
padding: 24px; padding: 24px;
overscroll-behavior: contain;
} }
.modal-card { .modal-card {
width: min(560px, 100%); width: min(560px, 100%);
@@ -713,6 +715,8 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
padding: 14px 16px 10px; padding: 14px 16px 10px;
overflow: auto; overflow: auto;
} }
.modal-body .settings-row { gap: 10px; }
.modal-body .key-input { min-height: 36px; }
.modal-actions-inline { .modal-actions-inline {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -1190,12 +1194,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
} }
.accounts-empty { .accounts-empty {
width: min(520px, 100%);
margin: clamp(56px, 12vh, 120px) auto 0;
padding: 32px;
border: 1px solid var(--border);
border-radius: 16px;
background: linear-gradient(180deg, rgba(62, 167, 255, 0.06), rgba(255, 255, 255, 0.025));
box-shadow: var(--panel-shadow);
text-align: center; text-align: center;
padding: 48px 16px;
color: var(--text-dim);
} }
.accounts-empty p { font-size: 14px; margin-bottom: 4px; } .accounts-empty-icon {
.accounts-empty .hint { font-size: 12px; } display: grid;
place-items: center;
width: 48px;
height: 48px;
margin: 0 auto 14px;
border-radius: 14px;
background: rgba(62, 167, 255, 0.14);
color: var(--accent-end);
font-size: 30px;
font-weight: 300;
}
.accounts-empty h3 { font-size: 17px; margin-bottom: 6px; }
.accounts-empty p { max-width: 390px; margin: 0 auto 18px; color: var(--text-muted); font-size: 13px; line-height: 1.55; }
/* History View */ /* History View */
.history-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); } .history-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); }
@@ -1292,7 +1313,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
font-size: 12px; font-size: 12px;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: all 0.3s; transition: opacity 0.3s, transform 0.3s;
z-index: 2000; z-index: 2000;
} }
.copy-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .copy-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@@ -1339,3 +1360,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); } ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
:where(button, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid var(--accent-end);
outline-offset: 2px;
}
.key-input:focus-visible,
.hs-input:focus-visible {
border-color: var(--accent);
outline: 2px solid rgba(101, 216, 255, 0.45);
outline-offset: 1px;
}
.menu-spinner input[type="text"]:focus-visible {
outline: 2px solid var(--accent-end);
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
+51 -3
View File
@@ -53,9 +53,15 @@ setTimeout(async () => {
const tabLabels = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.textContent.trim()).join("|")'); const tabLabels = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.textContent.trim()).join("|")');
check('Current tab labels present', tabLabels === 'Upload|Accounts|Einstellungen|Verlauf'); check('Current tab labels present', tabLabels === 'Upload|Accounts|Einstellungen|Verlauf');
const tabSemantics = await wc.executeJavaScript('document.querySelector(".tab-bar")?.getAttribute("role") + "|" + document.querySelector(".tab.active")?.getAttribute("aria-selected")');
check('Tab navigation exposes active state', tabSemantics === 'tablist|true');
const activeTab = await wc.executeJavaScript('document.querySelector(".tab.active")?.textContent?.trim()'); const activeTab = await wc.executeJavaScript('document.querySelector(".tab.active")?.textContent?.trim()');
check('Upload tab active by default', activeTab === 'Upload'); check('Upload tab active by default', activeTab === 'Upload');
const tabStops = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.tabIndex).join("|")');
check('Tab navigation exposes one keyboard stop', tabStops === '0|-1|-1|-1');
const dropVisible = await wc.executeJavaScript('document.getElementById("dropZone")?.style.display !== "none"'); const dropVisible = await wc.executeJavaScript('document.getElementById("dropZone")?.style.display !== "none"');
check('Drop zone visible (no files)', dropVisible); check('Drop zone visible (no files)', dropVisible);
@@ -71,6 +77,21 @@ setTimeout(async () => {
const version = await wc.executeJavaScript('document.getElementById("versionLabel")?.textContent'); const version = await wc.executeJavaScript('document.getElementById("versionLabel")?.textContent');
check('Version label present', version && version.startsWith('v')); check('Version label present', version && version.startsWith('v'));
const localizedQueueHeaders = await wc.executeJavaScript('[...document.querySelectorAll("#queueTable thead th")].map(el => el.childNodes[0]?.textContent.trim()).join("|")');
check('Upload table labels are consistently German', localizedQueueHeaders === 'Dateiname|Hochgeladen / Größe|Hoster|Status|Zeit|Rest|Geschwindigkeit|Fortschritt');
const localizedRecentTabs = await wc.executeJavaScript('[...document.querySelectorAll(".recent-tab")].map(el => el.textContent.trim()).join("|")');
check('Recent panel labels are consistently German', localizedRecentTabs === 'Dateien|Statistik');
const localizedStatusbar = await wc.executeJavaScript('["sbConnections", "sbQueueCount", "sbRemainingCount", "sbInProgressCount", "sbDoneCount", "sbErrorCount"].map(id => document.getElementById(id)?.textContent).join("|")');
check('Statusbar labels are consistently German', localizedStatusbar === 'Verbindungen 0|Gesamt 0|Verbleibend 0|Läuft 0|Fertig 0|Fehler 0');
const toolbarLabels = await wc.executeJavaScript('[...document.querySelectorAll("#queueCommandBar .toolbar-btn")].map(el => el.getAttribute("aria-label")).join("|")');
check('Upload toolbar actions have German accessible names', toolbarLabels === 'Alle Uploads starten|Ausgewählte Uploads starten|Ausgewählte Datei erneut hochladen|Ausgewählten Upload abbrechen|Aktive Uploads beenden und stoppen|Alle Uploads abbrechen|Ganz nach oben|Nach oben|Nach unten|Ganz nach unten');
const keyboardTab = await wc.executeJavaScript('document.getElementById("upload-tab").focus(); document.getElementById("upload-tab").dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })); document.querySelector(".tab.active")?.textContent?.trim() + "|" + document.activeElement?.id');
check('Arrow keys move and activate main tabs', keyboardTab === 'Accounts|accounts-tab');
const ctxHidden = await wc.executeJavaScript('document.getElementById("contextMenu")?.style.display'); const ctxHidden = await wc.executeJavaScript('document.getElementById("contextMenu")?.style.display');
check('Context menu hidden', ctxHidden === 'none'); check('Context menu hidden', ctxHidden === 'none');
@@ -88,7 +109,10 @@ setTimeout(async () => {
const addAccountEnabled = await wc.executeJavaScript('document.getElementById("addAccountBtn")?.disabled === false'); const addAccountEnabled = await wc.executeJavaScript('document.getElementById("addAccountBtn")?.disabled === false');
check('Add account button enabled', addAccountEnabled); check('Add account button enabled', addAccountEnabled);
await wc.executeJavaScript('document.getElementById("addAccountBtn").click()'); const emptyAccountAction = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]")?.textContent?.trim()');
check('Empty account state offers direct action', emptyAccountAction === 'Ersten Account hinzufügen');
await wc.executeJavaScript('document.querySelector("[data-account-empty-add]").focus(); document.querySelector("[data-account-empty-add]").click()');
await new Promise(r => setTimeout(r, 200)); await new Promise(r => setTimeout(r, 200));
const accountModalVisible = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display'); const accountModalVisible = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
@@ -97,6 +121,21 @@ setTimeout(async () => {
const accountModalTitle = await wc.executeJavaScript('document.getElementById("accountModalTitle")?.textContent'); const accountModalTitle = await wc.executeJavaScript('document.getElementById("accountModalTitle")?.textContent');
check('Account modal is in add mode', accountModalTitle === 'Account hinzufügen'); check('Account modal is in add mode', accountModalTitle === 'Account hinzufügen');
const accountModalSemantics = await wc.executeJavaScript('document.querySelector("#accountModal .modal-card")?.getAttribute("role") + "|" + document.querySelector("#accountModal .modal-card")?.getAttribute("aria-modal")');
check('Account modal exposes dialog semantics', accountModalSemantics === 'dialog|true');
const accountFormLabels = await wc.executeJavaScript('["accountHosterSelect", "accField_label", "accField_username", "accField_password"].every(id => document.getElementById(id)?.labels?.length === 1)');
check('Account form controls have linked labels', accountFormLabels);
const accountStatusLive = await wc.executeJavaScript('document.getElementById("accountModalStatus")?.getAttribute("aria-live")');
check('Account validation status is announced', accountStatusLive === 'polite');
const initialAccountFocus = await wc.executeJavaScript('document.activeElement?.id');
check('Account modal focuses first control', initialAccountFocus === 'accountHosterSelect');
const trappedAccountFocus = await wc.executeJavaScript('document.getElementById("saveAccountBtn").focus(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true })); document.activeElement?.id');
check('Account modal keeps keyboard focus inside', trappedAccountFocus === 'closeAccountModalBtn');
const authOptionCount = await wc.executeJavaScript('document.querySelectorAll("#accountHosterSelect option").length'); const authOptionCount = await wc.executeJavaScript('document.querySelectorAll("#accountHosterSelect option").length');
check('7 hoster authentication options exist', authOptionCount === 7); check('7 hoster authentication options exist', authOptionCount === 7);
@@ -109,9 +148,18 @@ setTimeout(async () => {
const credentialInputs = await wc.executeJavaScript('document.querySelectorAll("#accountCredsFields .key-input").length'); const credentialInputs = await wc.executeJavaScript('document.querySelectorAll("#accountCredsFields .key-input").length');
check('Credential inputs rendered', credentialInputs === 2); check('Credential inputs rendered', credentialInputs === 2);
await wc.executeJavaScript('document.getElementById("cancelAccountModalBtn").click()'); const passwordToggleState = await wc.executeJavaScript('document.querySelector("#accountCredsFields .toggle-vis").click(); document.querySelector("#accountCredsFields .toggle-vis").getAttribute("aria-label") + "|" + document.querySelector("#accountCredsFields .toggle-vis").getAttribute("aria-pressed")');
check('Password visibility action exposes its state', passwordToggleState === 'Passwort verbergen|true');
await wc.executeJavaScript('document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))');
const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display'); const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
check('Account modal closes', accountModalHidden === 'none'); check('Escape closes account modal', accountModalHidden === 'none');
const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add")');
check('Account modal restores trigger focus', restoredAccountFocus === true);
const fallbackAccountFocus = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]").focus(); document.querySelector("[data-account-empty-add]").click(); document.querySelector("[data-account-empty-add]").remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); document.activeElement?.id');
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
console.log('\\n=== Settings View ==='); console.log('\\n=== Settings View ===');