diff --git a/web/app.js b/web/app.js index ea12b2c..7e5b481 100644 --- a/web/app.js +++ b/web/app.js @@ -357,6 +357,8 @@ const bookmarkSortSelect = document.getElementById('bookmarkSortSelect'); const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle'); const profileSelectElement = document.getElementById('profileSelect'); const includeExpiredToggle = document.getElementById('includeExpiredToggle'); +const searchInput = document.getElementById('searchInput'); +const searchClearBtn = document.getElementById('searchClearBtn'); const mergeControls = document.getElementById('mergeControls'); const mergeModeToggle = document.getElementById('mergeModeToggle'); const mergeSubmitBtn = document.getElementById('mergeSubmitBtn'); @@ -540,6 +542,8 @@ let pendingAutoOpenTimerId = null; let pendingAutoOpenCountdownIntervalId = null; let pendingProcessingBatch = false; let pendingOpenCooldownMap = loadPendingOpenCooldownMap(currentProfile); +let pendingBulkStatusResetTimerId = null; +let lastPendingBulkHint = { openableCount: 0, cooldownBlocked: 0 }; function updateIncludeExpiredToggleVisibility() { if (!includeExpiredToggle) { @@ -583,27 +587,89 @@ function updateMergeControlsUI() { } } -function updatePendingBulkControls(filteredCount = 0) { +function clearPendingBulkStatusResetTimer() { + if (pendingBulkStatusResetTimerId) { + clearTimeout(pendingBulkStatusResetTimerId); + pendingBulkStatusResetTimerId = null; + } +} + +function buildPendingBulkHintText({ openableCount = 0, cooldownBlocked = 0 } = {}) { + const openable = Math.max(0, openableCount || 0); + const blocked = Math.max(0, cooldownBlocked || 0); + if (blocked > 0) { + return `Öffnungsbereit: ${openable} · Cooldown: ${blocked}`; + } + return `Öffnungsbereit: ${openable}`; +} + +function applyPendingBulkHintStatus() { + if (!pendingBulkStatus) { + return; + } + if (currentTab !== 'pending') { + pendingBulkStatus.textContent = ''; + pendingBulkStatus.classList.remove('bulk-status--hint', 'bulk-status--error'); + return; + } + pendingBulkStatus.textContent = buildPendingBulkHintText(lastPendingBulkHint); + pendingBulkStatus.classList.add('bulk-status--hint'); + pendingBulkStatus.classList.remove('bulk-status--error'); +} + +function updatePendingBulkControls(filteredCount = 0, stats = null) { if (!pendingBulkControls) { return; } const isPendingTab = currentTab === 'pending'; pendingBulkControls.hidden = !isPendingTab; pendingBulkControls.style.display = isPendingTab ? 'flex' : 'none'; + + if (!isPendingTab) { + clearPendingBulkStatusResetTimer(); + if (pendingBulkStatus) { + pendingBulkStatus.textContent = ''; + pendingBulkStatus.classList.remove('bulk-status--hint', 'bulk-status--error'); + } + } + + const normalizedStats = stats && typeof stats === 'object' + ? { + openableCount: Math.max(0, parseInt(stats.openableCount, 10) || 0), + cooldownBlocked: Math.max(0, parseInt(stats.cooldownBlocked, 10) || 0) + } + : { openableCount: Math.max(0, filteredCount || 0), cooldownBlocked: 0 }; + lastPendingBulkHint = normalizedStats; + if (pendingBulkOpenBtn) { - pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || filteredCount === 0; + pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || normalizedStats.openableCount === 0; + } + + if (isPendingTab) { + applyPendingBulkHintStatus(); } } function setPendingBulkStatus(message = '', isError = false) { + clearPendingBulkStatusResetTimer(); if (!pendingBulkStatus) { + if (message) { + showToast(message, isError ? 'error' : 'info'); + } return; } - pendingBulkStatus.textContent = ''; - pendingBulkStatus.classList.remove('bulk-status--error'); - if (message) { - showToast(message, isError ? 'error' : 'info'); + if (!message) { + applyPendingBulkHintStatus(); + return; } + pendingBulkStatus.textContent = message; + pendingBulkStatus.classList.toggle('bulk-status--error', !!isError); + pendingBulkStatus.classList.toggle('bulk-status--hint', !isError); + showToast(message, isError ? 'error' : 'info'); + pendingBulkStatusResetTimerId = setTimeout(() => { + pendingBulkStatusResetTimerId = null; + applyPendingBulkHintStatus(); + }, 4000); } function initializeFocusParams() { @@ -1949,15 +2015,65 @@ function normalizeRequiredProfiles(post) { return Array.from({ length: count }, (_, index) => index + 1); } +function getTabButtons() { + return Array.from(document.querySelectorAll('.tab-btn[data-tab]')); +} + function updateTabButtons() { - document.querySelectorAll('.tab-btn').forEach((button) => { - if (!button.dataset.tab) { - return; - } - button.classList.toggle('active', button.dataset.tab === currentTab); + getTabButtons().forEach((button) => { + const isActive = button.dataset.tab === currentTab; + button.classList.toggle('active', isActive); + button.setAttribute('aria-selected', isActive ? 'true' : 'false'); + button.setAttribute('tabindex', isActive ? '0' : '-1'); }); } +function handleTabKeydown(event) { + if (!event || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'Home' && event.key !== 'End')) { + return; + } + + const tabs = getTabButtons(); + if (!tabs.length) { + return; + } + + const currentIndex = tabs.findIndex((tab) => tab === event.currentTarget); + if (currentIndex === -1) { + return; + } + + event.preventDefault(); + let nextIndex = currentIndex; + + if (event.key === 'Home') { + nextIndex = 0; + } else if (event.key === 'End') { + nextIndex = tabs.length - 1; + } else if (event.key === 'ArrowRight') { + nextIndex = (currentIndex + 1) % tabs.length; + } else if (event.key === 'ArrowLeft') { + nextIndex = (currentIndex - 1 + tabs.length) % tabs.length; + } + + const nextTab = tabs[nextIndex]; + if (!nextTab || !nextTab.dataset.tab) { + return; + } + + setTab(nextTab.dataset.tab, { updateUrl: true }); + nextTab.focus(); +} + +function updateSearchClearButtonVisibility() { + if (!searchInput || !searchClearBtn) { + return; + } + const hasValue = typeof searchInput.value === 'string' && searchInput.value.trim().length > 0; + searchClearBtn.hidden = !hasValue; + searchClearBtn.disabled = !hasValue; +} + function isPostsViewActive() { const postsView = document.querySelector('[data-view="posts"]'); return Boolean(postsView && postsView.classList.contains('app-view--active')); @@ -2026,7 +2142,6 @@ function getPostListState() { const tabTotalCount = filteredItems.length; - const searchInput = document.getElementById('searchInput'); const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : ''; const searchActive = Boolean(searchValue); @@ -3651,10 +3766,11 @@ if (pendingAutoOpenToggle) { } // Tab switching -document.querySelectorAll('.tab-btn').forEach(btn => { +getTabButtons().forEach((btn) => { btn.addEventListener('click', () => { setTab(btn.dataset.tab, { updateUrl: true }); }); + btn.addEventListener('keydown', handleTabKeydown); }); window.addEventListener('app:view-change', (event) => { @@ -3715,14 +3831,44 @@ if (manualRefreshBtn) { }); } -const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.addEventListener('input', () => { + updateSearchClearButtonVisibility(); resetVisibleCount(); renderPosts(); }); + + document.addEventListener('keydown', (event) => { + if (!event || event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) { + return; + } + const target = event.target; + const tagName = target && target.tagName ? target.tagName.toLowerCase() : ''; + const isEditable = !!(target && (target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select')); + if (isEditable || !isPostsViewActive()) { + return; + } + event.preventDefault(); + searchInput.focus(); + searchInput.select(); + }); } +if (searchClearBtn) { + searchClearBtn.addEventListener('click', () => { + if (!searchInput) { + return; + } + searchInput.value = ''; + updateSearchClearButtonVisibility(); + resetVisibleCount(); + renderPosts(); + searchInput.focus(); + }); +} + +updateSearchClearButtonVisibility(); + if (mergeModeToggle) { mergeModeToggle.addEventListener('click', () => { if (currentTab !== 'all') { @@ -4129,7 +4275,22 @@ function renderPosts() { } updateFilteredCount(currentTab, filteredItems.length); - updatePendingBulkControls(filteredItems.length); + let pendingBulkStats = null; + if (currentTab === 'pending') { + pendingBulkStats = filteredItems.reduce((stats, item) => { + const post = item && item.post ? item.post : null; + if (!post || !post.url) { + return stats; + } + if (isPendingOpenCooldownActive(post.id)) { + stats.cooldownBlocked += 1; + } else { + stats.openableCount += 1; + } + return stats; + }, { openableCount: 0, cooldownBlocked: 0 }); + } + updatePendingBulkControls(filteredItems.length, pendingBulkStats); const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab)); const visibleItems = filteredItems.slice(0, visibleCount); diff --git a/web/index.html b/web/index.html index 8e08098..51a5feb 100644 --- a/web/index.html +++ b/web/index.html @@ -214,9 +214,9 @@