diff --git a/kjvstudy_org/static/sw.js b/kjvstudy_org/static/sw.js index da31beb..370b5f4 100644 --- a/kjvstudy_org/static/sw.js +++ b/kjvstudy_org/static/sw.js @@ -169,8 +169,8 @@ async function startBackgroundCaching() { }); // Concurrent pool - keep N requests in flight at all times - const CONCURRENCY = 50; // Number of concurrent requests - const PROGRESS_INTERVAL = 100; // Notify every N completions + const CONCURRENCY = 100; // Number of concurrent requests + const PROGRESS_INTERVAL = 250; // Notify every N completions let nextIndex = 0; let lastNotified = 0; diff --git a/kjvstudy_org/templates/offline.html b/kjvstudy_org/templates/offline.html index 71d1c30..c2bda7a 100644 --- a/kjvstudy_org/templates/offline.html +++ b/kjvstudy_org/templates/offline.html @@ -330,6 +330,77 @@ font-size: 1rem; color: var(--text-secondary); } + /* Collapsible URL sections */ + .url-categories { + margin: 1rem 0; + } + .url-category { + margin-bottom: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 6px; + overflow: hidden; + } + .url-category-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: var(--border-color); + cursor: pointer; + user-select: none; + font-weight: 500; + } + .url-category-header:hover { + background: var(--text-secondary); + color: var(--bg-color); + } + .url-category.selected .url-category-header { + outline: 2px solid var(--success-color); + outline-offset: -2px; + } + .url-category-toggle { + font-size: 0.8rem; + transition: transform 0.2s; + } + .url-category.expanded .url-category-toggle { + transform: rotate(90deg); + } + .url-category-content { + display: none; + max-height: 400px; + overflow-y: auto; + padding: 0.5rem; + background: var(--bg-color); + } + .url-category.expanded .url-category-content { + display: block; + } + .url-item { + display: block; + padding: 0.4rem 0.75rem; + color: var(--link-color); + text-decoration: none; + font-size: 0.9rem; + border-radius: 4px; + } + .url-item:hover { + background: var(--border-color); + } + .url-item.selected { + background: var(--success-color); + color: white; + } + .url-count { + font-size: 0.8rem; + color: var(--text-secondary); + font-weight: normal; + } + .url-nav-hint { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 0.5rem; + font-style: italic; + }
@@ -407,7 +478,8 @@Use j/k to navigate categories, Enter/Space to expand, then j/k to browse URLs
+Scanning cache...
No pages cached yet. Browse the site while online to cache pages.
'; + // Initialize URL navigation + initUrlNavigation(); + } catch (err) { cacheStatus.innerHTML = '✗ Error: ' + err.message + ''; cachedLinks.innerHTML = 'Could not read cache.
'; @@ -1034,6 +1144,234 @@ }); loadBibleData(); + + // URL list navigation state + let urlNavMode = false; + let selectedCategoryIdx = -1; + let selectedUrlIdx = -1; + + // Toggle category expand/collapse + window.toggleCategory = function(categoryEl) { + categoryEl.classList.toggle('expanded'); + }; + + // Initialize URL navigation + window.initUrlNavigation = function() { + const categories = document.querySelectorAll('.url-category'); + if (categories.length === 0) return; + + // Add click handlers for URL items + document.querySelectorAll('.url-item').forEach(item => { + item.addEventListener('click', function(e) { + // Let the link work normally + }); + }); + }; + + function getUrlCategories() { + return Array.from(document.querySelectorAll('.url-category')); + } + + function getUrlsInCategory(categoryEl) { + return Array.from(categoryEl.querySelectorAll('.url-item')); + } + + function clearUrlSelection() { + document.querySelectorAll('.url-category.selected').forEach(el => el.classList.remove('selected')); + document.querySelectorAll('.url-item.selected').forEach(el => el.classList.remove('selected')); + } + + function selectCategory(index) { + const categories = getUrlCategories(); + if (categories.length === 0) return; + + clearUrlSelection(); + selectedUrlIdx = -1; + + if (index < 0) index = 0; + if (index >= categories.length) index = categories.length - 1; + + selectedCategoryIdx = index; + categories[index].classList.add('selected'); + categories[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + + function selectUrlItem(categoryIndex, urlIndex) { + const categories = getUrlCategories(); + if (categoryIndex < 0 || categoryIndex >= categories.length) return; + + clearUrlSelection(); + selectedCategoryIdx = categoryIndex; + + const urls = getUrlsInCategory(categories[categoryIndex]); + if (urls.length === 0) return; + + if (urlIndex < 0) urlIndex = 0; + if (urlIndex >= urls.length) urlIndex = urls.length - 1; + + selectedUrlIdx = urlIndex; + categories[categoryIndex].classList.add('selected'); + urls[urlIndex].classList.add('selected'); + urls[urlIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + + function expandCategory(index) { + const categories = getUrlCategories(); + if (index >= 0 && index < categories.length) { + categories[index].classList.add('expanded'); + } + } + + function collapseCategory(index) { + const categories = getUrlCategories(); + if (index >= 0 && index < categories.length) { + categories[index].classList.remove('expanded'); + } + } + + function isCategoryExpanded(index) { + const categories = getUrlCategories(); + if (index >= 0 && index < categories.length) { + return categories[index].classList.contains('expanded'); + } + return false; + } + + // Handle URL list keyboard navigation when in Technical Details + document.addEventListener('keydown', function(e) { + // Only handle if technical details is open + const details = document.querySelector('.debug-details'); + if (!details || !details.open) return; + + // Skip if in form elements + if (e.target.tagName === 'SELECT' || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + + const categories = getUrlCategories(); + if (categories.length === 0) return; + + // Check if we're in the URL navigation zone + const cachedLinksSection = document.getElementById('cached-links'); + const rect = cachedLinksSection.getBoundingClientRect(); + const isVisible = rect.top < window.innerHeight && rect.bottom > 0; + + if (!isVisible && selectedCategoryIdx < 0) return; + + // u key to enter URL navigation mode + if (e.key === 'u') { + e.preventDefault(); + urlNavMode = true; + if (selectedCategoryIdx < 0) { + selectCategory(0); + } + return; + } + + if (!urlNavMode && selectedCategoryIdx < 0) return; + + if (e.key === 'j' || e.key === 'ArrowDown') { + // If URL mode is active + if (urlNavMode || selectedCategoryIdx >= 0) { + e.preventDefault(); + e.stopPropagation(); + + if (selectedUrlIdx >= 0 && isCategoryExpanded(selectedCategoryIdx)) { + // Navigate within URLs + const urls = getUrlsInCategory(categories[selectedCategoryIdx]); + if (selectedUrlIdx < urls.length - 1) { + selectUrlItem(selectedCategoryIdx, selectedUrlIdx + 1); + } else { + // Move to next category + if (selectedCategoryIdx < categories.length - 1) { + collapseCategory(selectedCategoryIdx); + selectCategory(selectedCategoryIdx + 1); + } + } + } else if (selectedCategoryIdx >= 0) { + // Navigate categories + if (selectedCategoryIdx < categories.length - 1) { + selectCategory(selectedCategoryIdx + 1); + } + } else { + selectCategory(0); + } + } + } else if (e.key === 'k' || e.key === 'ArrowUp') { + if (urlNavMode || selectedCategoryIdx >= 0) { + e.preventDefault(); + e.stopPropagation(); + + if (selectedUrlIdx >= 0 && isCategoryExpanded(selectedCategoryIdx)) { + // Navigate within URLs + if (selectedUrlIdx > 0) { + selectUrlItem(selectedCategoryIdx, selectedUrlIdx - 1); + } else { + // Back to category selection + selectedUrlIdx = -1; + selectCategory(selectedCategoryIdx); + } + } else if (selectedCategoryIdx > 0) { + selectCategory(selectedCategoryIdx - 1); + } + } + } else if (e.key === 'Enter' || e.key === ' ') { + if (selectedUrlIdx >= 0) { + // Open selected URL + const urls = getUrlsInCategory(categories[selectedCategoryIdx]); + if (urls[selectedUrlIdx]) { + window.location.href = urls[selectedUrlIdx].href; + } + } else if (selectedCategoryIdx >= 0) { + e.preventDefault(); + e.stopPropagation(); + // Toggle category expansion + if (isCategoryExpanded(selectedCategoryIdx)) { + collapseCategory(selectedCategoryIdx); + } else { + expandCategory(selectedCategoryIdx); + // Select first URL in category + const urls = getUrlsInCategory(categories[selectedCategoryIdx]); + if (urls.length > 0) { + selectUrlItem(selectedCategoryIdx, 0); + } + } + } + } else if (e.key === 'Escape') { + if (selectedUrlIdx >= 0) { + // Back to category + e.preventDefault(); + selectedUrlIdx = -1; + selectCategory(selectedCategoryIdx); + } else if (selectedCategoryIdx >= 0) { + // Exit URL nav mode + e.preventDefault(); + clearUrlSelection(); + selectedCategoryIdx = -1; + urlNavMode = false; + } + } else if (e.key === 'l' || e.key === 'ArrowRight') { + if (selectedCategoryIdx >= 0 && !isCategoryExpanded(selectedCategoryIdx)) { + e.preventDefault(); + e.stopPropagation(); + expandCategory(selectedCategoryIdx); + const urls = getUrlsInCategory(categories[selectedCategoryIdx]); + if (urls.length > 0) { + selectUrlItem(selectedCategoryIdx, 0); + } + } + } else if (e.key === 'h' || e.key === 'ArrowLeft') { + if (selectedCategoryIdx >= 0) { + e.preventDefault(); + e.stopPropagation(); + if (selectedUrlIdx >= 0) { + // Go back to category header + selectedUrlIdx = -1; + selectCategory(selectedCategoryIdx); + } else if (isCategoryExpanded(selectedCategoryIdx)) { + collapseCategory(selectedCategoryIdx); + } + } + } + }, true); // Use capture to get events before other handlers })();