From ea09993df9d5cbc99fb3b65b7b8ee46572eb2f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 21 Aug 2026 23:05:23 +0300 Subject: [PATCH] fix: IDB resilience, direct autopreload, nav delegation, Bible bounds --- src/core/data-cache.ts | 15 +++++++++-- src/core/offline-manager.ts | 15 ++++++++++- src/ui/app.ts | 2 +- src/ui/bible-view.ts | 11 +++++--- src/ui/settings-view.ts | 52 ++++++++++++++++++++++++++++++++----- 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/core/data-cache.ts b/src/core/data-cache.ts index c95c6fb..dce18cc 100644 --- a/src/core/data-cache.ts +++ b/src/core/data-cache.ts @@ -39,8 +39,19 @@ function getDB(): Promise { store.createIndex('storedAt', 'storedAt', { unique: false }); } }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); + request.onsuccess = () => { + const db = request.result; + db.onclose = () => { dbPromise = null; }; + db.onversionchange = () => { + db.close(); + dbPromise = null; + }; + resolve(db); + }; + request.onerror = () => { + dbPromise = null; + reject(request.error); + }; }); } return dbPromise; diff --git a/src/core/offline-manager.ts b/src/core/offline-manager.ts index 55b25dc..62591bf 100644 --- a/src/core/offline-manager.ts +++ b/src/core/offline-manager.ts @@ -64,6 +64,19 @@ function loadManifest(): Promise> | null return manifestPromise; } +function getFallbackCalendarFiles(lang: string): string[] { + const files = [ + `/data/${lang}/menaion-bundle.json`, + '/data/shared/fasting.json', + '/data/shared/menaion-daily/index.json', + ]; + for (let m = 1; m <= 12; m++) { + const mm = String(m).padStart(2, '0'); + files.push(`/data/${lang}/lives/${mm}.json`); + } + return files; +} + export class OfflineManager { private static _progress: PreloadProgress | null = null; @@ -104,7 +117,7 @@ export class OfflineManager { for (const lang of options.languages) { for (const type of options.types) { if (type === 'calendar') { - const list = m?.[lang]?.calendar ?? []; + const list = m?.[lang]?.calendar ?? getFallbackCalendarFiles(lang); filesToFetch.push(...list); } else if (type === 'bible') { filesToFetch.push('/data/bible/versions.json'); diff --git a/src/ui/app.ts b/src/ui/app.ts index e01f6f6..5e79d58 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -72,7 +72,7 @@ export class App { document.addEventListener('touchstart', closeOpenDropdowns, { passive: true }); // Close library dropdown when a nav link inside it is clicked - document.querySelector('nav')?.addEventListener('click', (e) => { + document.addEventListener('click', (e) => { const target = e.target as HTMLElement; const link = target.closest('.nav-link'); if (link) { diff --git a/src/ui/bible-view.ts b/src/ui/bible-view.ts index 217ca3b..3c1a0b8 100644 --- a/src/ui/bible-view.ts +++ b/src/ui/bible-view.ts @@ -236,6 +236,9 @@ export class BibleView { const select = document.getElementById('book-select') as HTMLSelectElement; if (!select) return; const books = this.getBooksForVersion(); + if (!this.currentBook || !books.some(b => b.id === this.currentBook)) { + this.currentBook = books[0]?.id || 'Gen'; + } select.innerHTML = books.map(b => `` ).join(''); @@ -247,12 +250,14 @@ export class BibleView { if (!select) return; const books = this.getBooksForVersion(); const book = books.find(b => b.id === this.currentBook); - const chapters = book?.chapters ?? 50; + const chapters = (book && Number.isInteger(book.chapters) && book.chapters > 0) ? book.chapters : 50; + const parsedCh = parseInt(this.currentPassage, 10); + const selectedCh = isNaN(parsedCh) || parsedCh < 1 ? 1 : parsedCh; select.innerHTML = Array.from({ length: chapters }, (_, i) => - `` + `` ).join(''); // Sync passage to selected chapter - this.currentPassage = select.value; + this.currentPassage = select.value || '1'; } private async loadReading() { diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index ae15fad..a5ffd66 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -636,12 +636,52 @@ export class SettingsView { if (this.action === 'autopreload') { document.querySelectorAll('.offline-lang').forEach(el => (el as HTMLInputElement).checked = true); document.querySelectorAll('.offline-type').forEach(el => (el as HTMLInputElement).checked = true); - const bibleChevron = document.getElementById('bible-chevron'); - if (bibleChevron) bibleChevron.click(); - setTimeout(() => { - document.querySelectorAll('.bible-trans').forEach(el => (el as HTMLInputElement).checked = true); - document.getElementById('offline-preload')?.click(); - }, 300); + const progressDiv = document.getElementById('offline-progress'); + const progressBar = document.getElementById('offline-progress-bar'); + const progressText = document.getElementById('offline-progress-text'); + if (progressDiv) progressDiv.classList.remove('hidden'); + + const selectedLangs = LANGUAGES.map(l => l.code); + const selectedTypes: ('calendar' | 'bible')[] = ['calendar', 'bible']; + + let bibleTrans: string[] = []; + try { + const resp = await fetch('/data/bible/versions.json'); + const versions = await resp.json(); + bibleTrans = versions.map((v: any) => v.id); + } catch {} + + OfflineManager.preload({ + languages: selectedLangs, + types: selectedTypes, + bibleTranslations: bibleTrans, + }).then((prog) => { + if (progressBar) progressBar.style.width = '100%'; + if (progressText) { + progressText.textContent = prog.failed > 0 + ? `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}` + : t.settings.offlineDone; + } + OfflineManager.getStats().then(stats => { + const statsEl = document.getElementById('offline-stats'); + if (statsEl) { + statsEl.textContent = t.settings.offlineCacheInfo + .replace('{0}', OfflineManager.formatBytes(stats.usageBytes)) + .replace('{2}', String(stats.cachedFiles)); + } + }); + }); + + const pollInterval = setInterval(() => { + const prog = OfflineManager.getProgress(); + if (prog && prog.total > 0) { + const pct = Math.round((prog.current / prog.total) * 100); + if (progressBar) progressBar.style.width = `${pct}%`; + if (progressText) progressText.textContent = `${prog.current} / ${prog.total} ${t.settings.offlineDataTypes} — ${prog.file.substring(0, 60)}...`; + } + if (!prog || prog.done) clearInterval(pollInterval); + }, 200); + window.history.replaceState(null, '', '#settings'); } })();