diff --git a/renderer/app.js b/renderer/app.js index 83da3a7..4d681a2 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -229,18 +229,43 @@ function _isHistoryTabActive() { const tab = document.querySelector('.tab.active'); return !!(tab && tab.dataset.view === 'history'); } -document.querySelectorAll('.tab').forEach(tab => { - tab.addEventListener('click', () => { - document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); - document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); +// Cache the tab/view collections once and use event delegation on the parent +// so tab switches don't trigger three querySelectorAll walks per click. +(() => { + const tabs = Array.from(document.querySelectorAll('.tab')); + const views = Array.from(document.querySelectorAll('.view')); + const tabsByView = {}; + const viewsById = {}; + for (const t of tabs) tabsByView[t.dataset.view] = t; + for (const v of views) viewsById[v.id] = v; + let activeTab = tabs.find(t => t.classList.contains('active')) || tabs[0]; + + const handle = (target) => { + const tab = target.closest('.tab'); + if (!tab || tab === activeTab) return; + if (activeTab) { + activeTab.classList.remove('active'); + const prevView = viewsById[`${activeTab.dataset.view}-view`]; + if (prevView) prevView.classList.remove('active'); + } tab.classList.add('active'); - document.getElementById(`${tab.dataset.view}-view`).classList.add('active'); + const nextView = viewsById[`${tab.dataset.view}-view`]; + if (nextView) nextView.classList.add('active'); + activeTab = tab; if (tab.dataset.view === 'history') { _historyDirty = false; loadHistory(); } - }); -}); + }; + + const tabBar = tabs[0] && tabs[0].parentElement; + if (tabBar) { + tabBar.addEventListener('click', (e) => handle(e.target)); + } else { + // Fallback: bind per-tab if somehow no common parent + tabs.forEach(t => t.addEventListener('click', () => handle(t))); + } +})(); // --- Hoster selection --- function accountHasCreds(name, account) { @@ -1175,6 +1200,22 @@ function handleRowClick(e, row) { // --- Context menu --- let alwaysOnTopState = false; +// Cache hoster-counts for the context menu. Invalidated on structural changes +// to queueJobs (the length-based signature is good enough — a job's hoster +// never changes after it's created). +let _hosterCountsCache = { sig: '', result: new Map() }; +function _getHosterCounts() { + const sig = `${queueJobs.length}`; + if (_hosterCountsCache.sig === sig) return _hosterCountsCache.result; + const m = new Map(); + for (let i = 0; i < queueJobs.length; i++) { + const h = queueJobs[i].hoster; + m.set(h, (m.get(h) || 0) + 1); + } + _hosterCountsCache = { sig, result: m }; + return m; +} + function handleRowContextMenu(e, row) { e.preventDefault(); const jobId = row.dataset.jobId; @@ -1204,11 +1245,11 @@ function showContextMenu(x, y) { const startItem = menu.querySelector('[data-action="start-selected"]'); if (startItem) startItem.textContent = n > 1 ? `Ausgewählte starten (${n})` : 'Ausgewählte starten'; - // Dynamic "delete by hoster" submenu + // Dynamic "delete by hoster" submenu — cached count keyed by queue length + // so a right-click on a 5000-job queue doesn't rescan everything. const deleteHosterSubmenu = menu.querySelector('.ctx-hoster-delete-submenu'); const deleteHosterContainer = menu.querySelector('.ctx-hoster-delete-items'); - const hosterCounts = new Map(); - queueJobs.forEach(j => hosterCounts.set(j.hoster, (hosterCounts.get(j.hoster) || 0) + 1)); + const hosterCounts = _getHosterCounts(); deleteHosterContainer.innerHTML = ''; if (hosterCounts.size > 0) { deleteHosterSubmenu.style.display = ''; @@ -2698,10 +2739,17 @@ async function saveSettings(options = {}) { newHosterSettings[name] = hs; } - await window.api.saveHosterSettings(newHosterSettings); - await window.api.saveGlobalSettings(globalSettings); - config = await window.api.getConfig(); - hosterSettings = config.hosterSettings || {}; + // Fire both saves in parallel instead of serializing the two IPC round-trips. + // Skip the getConfig refetch — we just wrote it, we know the new state, and + // the round-trip added 100–200ms of UI stall per keystroke (autosave fires + // on every input change). + await Promise.all([ + window.api.saveHosterSettings(newHosterSettings), + window.api.saveGlobalSettings(globalSettings) + ]); + config.hosterSettings = newHosterSettings; + config.globalSettings = globalSettings; + hosterSettings = newHosterSettings; clearTimeout(settingsSaveTimer); // Start/stop folder monitor based on settings @@ -3358,9 +3406,11 @@ function renderRecentUploadsPanel() { if (selectedRecentIds.has(id)) selectedRecentIds.delete(id); else selectedRecentIds.add(id); } else if (e.shiftKey && selectedRecentIds.size > 0) { - // Use already-sorted DOM order (cheap) instead of re-sorting the full array. - const sortedOrders = Array.from(tbody.querySelectorAll('.recent-file-row')) - .map(r => parseInt(r.dataset.order, 10)); + // Reuse the already-sorted array from the sort cache instead of + // querying every .recent-file-row in the DOM (O(visible) vs O(N) + // on large panels). + const sortedOrders = (_recentSortCache.result || sortRecentFiles(sessionFilesData)) + .map(r => r.order); const lastIdx = sortedOrders.findIndex(o => selectedRecentIds.has(o)); const curIdx = sortedOrders.indexOf(id); if (lastIdx >= 0 && curIdx >= 0) {