diff --git a/package-lock.json b/package-lock.json index 183c1f5..c0b4b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-hoster-uploader", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-hoster-uploader", - "version": "2.0.1", + "version": "2.0.2", "dependencies": { "chokidar": "^3.6.0", "undici": "^7.29.0", diff --git a/package.json b/package.json index 6a1a234..c373980 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "2.0.1", + "version": "2.0.2", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index 068bb99..51d679a 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -353,10 +353,14 @@ function _isHistoryTabActive() { if (!tab || tab === activeTab) return; if (activeTab) { activeTab.classList.remove('active'); + activeTab.setAttribute('aria-selected', 'false'); + activeTab.tabIndex = -1; const prevView = viewsById[`${activeTab.dataset.view}-view`]; if (prevView) prevView.classList.remove('active'); } tab.classList.add('active'); + tab.setAttribute('aria-selected', 'true'); + tab.tabIndex = 0; const nextView = viewsById[`${tab.dataset.view}-view`]; if (nextView) nextView.classList.add('active'); activeTab = tab; @@ -368,6 +372,18 @@ function _isHistoryTabActive() { const tabBar = tabs[0] && tabs[0].parentElement; if (tabBar) { 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 { // Fallback: bind per-tab if somehow no common parent tabs.forEach(t => t.addEventListener('click', () => handle(t))); @@ -1920,9 +1936,20 @@ document.addEventListener('click', (e) => { if (!e.target.closest('.context-menu')) hideContextMenu(); }); 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') { hideContextMenu(); cancelHosterModal(); + if (accountModal && accountModal.style.display !== 'none') closeAccountModal(); } if (e.target.closest('input, textarea, select')) return; const activeView = document.querySelector('.view.active'); @@ -2951,12 +2978,12 @@ function updateStatusBar() { const totalSize = Math.max(stats.totalSize, _sessionTotalBytes); document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`; document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`; - document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`; - document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`; - document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`; - document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`; - document.getElementById('sbDoneCount').textContent = `Done: ${_sessionDoneCount}`; - document.getElementById('sbErrorCount').textContent = `Error: ${_sessionErrorCount}`; + document.getElementById('sbConnections').textContent = `Verbindungen ${lastUploadStats.activeJobs || 0}`; + document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`; + document.getElementById('sbRemainingCount').textContent = `Verbleibend ${stats.remaining}`; + document.getElementById('sbInProgressCount').textContent = `Läuft ${stats.inProgress}`; + document.getElementById('sbDoneCount').textContent = `Fertig ${_sessionDoneCount}`; + document.getElementById('sbErrorCount').textContent = `Fehler ${_sessionErrorCount}`; } // --- Health Check --- @@ -3864,8 +3891,10 @@ function renderAccounts() { if (allAccounts.length === 0) { container.innerHTML = `
-

Keine Accounts vorhanden

- Klicke auf "Account hinzufügen", um einen Hoster einzurichten. + +

Noch keine Accounts

+

Füge deinen ersten Hoster-Account hinzu. Die Zugangsdaten werden vor dem Speichern geprüft.

+
`; if (footer) footer.style.display = 'none'; if (!_accountListenersBound) bindAccountListeners(container); @@ -4106,6 +4135,7 @@ function bindAccountListeners(container) { } const btn = e.target.closest('button'); 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.accountEdit) return openAccountModal(btn.dataset.accountEdit); if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete); @@ -4240,25 +4270,42 @@ function getCredsFieldsHtml(authType, account, hoster) { }; return `
- - + +
- - - + + +
`; } // API key return `
- - - + + +
`; } +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) { + _accountModalReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; editingAccountId = editAccountId || null; _resetAccountModalState(); const modal = document.getElementById('accountModal'); @@ -4298,17 +4345,17 @@ function openAccountModal(editAccountId) { credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {}, firstOpt.value); } - // Toggle visibility buttons - credsContainer.querySelectorAll('.toggle-vis').forEach(btn => { - btn.addEventListener('click', () => { - const input = btn.previousElementSibling; - input.type = input.type === 'password' ? 'text' : 'password'; - }); - }); + wireCredentialVisibilityButtons(credsContainer); _wireCredFieldInvalidation(); modal.style.display = 'flex'; + requestAnimationFrame(() => { + const firstControl = editingAccountId + ? document.getElementById('accField_label') + : hosterSelect; + if (firstControl) firstControl.focus(); + }); } function closeAccountModal() { @@ -4316,6 +4363,12 @@ function closeAccountModal() { _hideOtpField(); editingAccountId = null; _resetAccountModalState(); + const returnFocus = _accountModalReturnFocus; + _accountModalReturnFocus = null; + const focusTarget = returnFocus && returnFocus.isConnected + ? returnFocus + : document.getElementById('addAccountBtn'); + if (focusTarget) focusTarget.focus(); } function openDeleteAccountModal(accountId) { @@ -4879,7 +4932,7 @@ function renderHistoryTable(container) { }; container.innerHTML = ` - ${headerCell('date', 'Date')}${headerCell('filename', 'Filename')}${headerCell('host', 'Host')}${headerCell('link', 'Link')} + ${headerCell('date', 'Datum')}${headerCell('filename', 'Dateiname')}${headerCell('host', 'Hoster')}${headerCell('link', 'Link')}
`; if (!_historyListenersBound) { @@ -5136,12 +5189,7 @@ function setupListeners() { const authType = opt ? opt.authType : 'login'; const credsContainer = document.getElementById('accountCredsFields'); credsContainer.innerHTML = getCredsFieldsHtml(authType, {}, e.target.value); - credsContainer.querySelectorAll('.toggle-vis').forEach(btn => { - btn.addEventListener('click', () => { - const input = btn.previousElementSibling; - input.type = input.type === 'password' ? 'text' : 'password'; - }); - }); + wireCredentialVisibilityButtons(credsContainer); _wireCredFieldInvalidation(); }); @@ -5185,7 +5233,7 @@ function showUpdateBanner(info) { function handleUpdateProgress(data) { const msg = document.getElementById('updateMessage'); 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 === 'launching') msg.textContent = 'Setup wird gestartet...'; else if (data.stage === 'done') msg.textContent = 'Update installiert. App wird neu gestartet...'; diff --git a/renderer/index.html b/renderer/index.html index 45f6e26..211349d 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -66,11 +66,11 @@ -