feat: Bible translation preloading with translation picker

This commit is contained in:
catarrh committed 2026-08-02 14:42:34 +03:00
1 parent 04eaa7058c
commit c2d5bd3093
3 files changed
+79 -5

No files matched your search

+4
View File
@@ -494,6 +494,7 @@ export interface Translations {
offlineLives: string; offlineLives: string;
offlineCalendar: string; offlineCalendar: string;
offlineMenaion: string; offlineMenaion: string;
offlineBible: string;
offlinePreload: string; offlinePreload: string;
offlineClearCache: string; offlineClearCache: string;
offlineCalculating: string; offlineCalculating: string;
@@ -1041,6 +1042,7 @@ const translations: Record<LanguageCode, Translations> = {
offlineLives: 'Lives', offlineLives: 'Lives',
offlineCalendar: 'Calendar', offlineCalendar: 'Calendar',
offlineMenaion: 'Menaion', offlineMenaion: 'Menaion',
offlineBible: 'Bible',
offlinePreload: 'Preload', offlinePreload: 'Preload',
offlineClearCache: 'Clear cache', offlineClearCache: 'Clear cache',
offlineCalculating: 'Calculating storage...', offlineCalculating: 'Calculating storage...',
@@ -1586,6 +1588,7 @@ const translations: Record<LanguageCode, Translations> = {
offlineLives: 'Жития', offlineLives: 'Жития',
offlineCalendar: 'Календарь', offlineCalendar: 'Календарь',
offlineMenaion: 'Минея', offlineMenaion: 'Минея',
offlineBible: 'Библия',
offlinePreload: 'Загрузить', offlinePreload: 'Загрузить',
offlineClearCache: 'Очистить кэш', offlineClearCache: 'Очистить кэш',
offlineCalculating: 'Расчёт размера...', offlineCalculating: 'Расчёт размера...',
@@ -2131,6 +2134,7 @@ const translations: Record<LanguageCode, Translations> = {
offlineLives: 'Житїя', offlineLives: 'Житїя',
offlineCalendar: 'Календарь', offlineCalendar: 'Календарь',
offlineMenaion: 'Минїа', offlineMenaion: 'Минїа',
offlineBible: 'Би́блїа',
offlinePreload: 'Загрузити', offlinePreload: 'Загрузити',
offlineClearCache: 'Очистити кешъ', offlineClearCache: 'Очистити кешъ',
offlineCalculating: 'Расчётъ размѣра...', offlineCalculating: 'Расчётъ размѣра...',
+43 -4
View File
@@ -19,6 +19,7 @@ import { DataCache } from './data-cache';
export interface PreloadOptions { export interface PreloadOptions {
languages: string[]; languages: string[];
types: ('lives' | 'bible' | 'calendar' | 'menaion')[]; types: ('lives' | 'bible' | 'calendar' | 'menaion')[];
bibleTranslations?: string[];
} }
export interface PreloadProgress { export interface PreloadProgress {
@@ -36,6 +37,15 @@ export interface CacheStats {
cachedTypes: string[]; cachedTypes: string[];
} }
/** Bible translation sizes (pre-computed). */
const BIBLE_SIZES: Record<string, string> = {
'en/bible/kjv': '5.1 MB', 'en/bible/brenton': '4.1 MB', 'en/bible/nonkjv': '0.9 MB',
'ru/bible/synod': '7.3 MB', 'ru/bible/kassian': '1.5 MB', 'ru/bible/yungerov': '1.7 MB',
'cu/bible/elis': '8.4 MB', 'el/bible/spt': '8.6 MB', 'fr/bible/ls': '5.2 MB',
'la/bible/vulgate': '4.4 MB', 'ar/bible/svd': '7.9 MB',
'zh/Hans/bible/cuv': '3.7 MB', 'zh/Hant/bible/cuv': '3.7 MB',
};
/** Data files to preload per language, grouped by type. */ /** Data files to preload per language, grouped by type. */
const DATA_FILES: Record<string, (lang: string) => string[]> = { const DATA_FILES: Record<string, (lang: string) => string[]> = {
lives: (lang) => { lives: (lang) => {
@@ -59,17 +69,35 @@ const DATA_FILES: Record<string, (lang: string) => string[]> = {
menaion: (lang) => [ menaion: (lang) => [
`/data/${lang}/menaion-bundle.json`, `/data/${lang}/menaion-bundle.json`,
], ],
bible: (lang) => [ bible: (_lang) => [
// Bible version index
'/data/bible/versions.json', '/data/bible/versions.json',
// Main Bible text files per language
`/data/${lang}/bible/synod.text`,
], ],
}; };
export class OfflineManager { export class OfflineManager {
private static _progress: PreloadProgress | null = null; private static _progress: PreloadProgress | null = null;
/** Get size string for a Bible translation. */
static getBibleSize(translationPath: string): string {
return BIBLE_SIZES[translationPath] || '?';
}
/** Discover all book files for a Bible translation from versions.json. */
static async getBibleFiles(translationPath: string): Promise<string[]> {
const files: string[] = [];
try {
const resp = await fetch('/data/bible/versions.json');
const versions = await resp.json() as any[];
const ver = versions.find((v: any) => v.id === translationPath);
if (ver && ver.books) {
for (const book of ver.books) {
files.push(`/data/${translationPath}/${book.id}.text`);
}
}
} catch {}
return files;
}
/** Get current preload progress */ /** Get current preload progress */
static getProgress(): PreloadProgress | null { static getProgress(): PreloadProgress | null {
return OfflineManager._progress; return OfflineManager._progress;
@@ -94,6 +122,17 @@ export class OfflineManager {
// Deduplicate // Deduplicate
const uniqueFiles = [...new Set(filesToFetch)]; const uniqueFiles = [...new Set(filesToFetch)];
// Add Bible translation files
for (const trans of options.bibleTranslations || []) {
const bibleFiles = await OfflineManager.getBibleFiles(trans);
for (const f of bibleFiles) {
if (!uniqueFiles.includes(f)) uniqueFiles.push(f);
}
}
if (!uniqueFiles.includes('/data/bible/versions.json')) {
uniqueFiles.unshift('/data/bible/versions.json');
}
const progress: PreloadProgress = { const progress: PreloadProgress = {
current: 0, current: 0,
total: uniqueFiles.length, total: uniqueFiles.length,
+32 -1
View File
@@ -292,7 +292,11 @@ export class SettingsView {
<label class="flex items-center gap-1 cursor-pointer"> <label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" value="menaion" class="offline-type accent-gold"> <span class="text-sm">${t.settings.offlineMenaion}</span> <input type="checkbox" value="menaion" class="offline-type accent-gold"> <span class="text-sm">${t.settings.offlineMenaion}</span>
</label> </label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" value="bible" class="offline-type accent-gold"> <span class="text-sm">${t.settings.offlineBible}</span>
</label>
</div> </div>
<div id="bible-translations" class="hidden ml-4 mt-1 space-y-1 border-l border-gold/20 pl-3"></div>
</div> </div>
<!-- Storage info and controls --> <!-- Storage info and controls -->
@@ -409,6 +413,26 @@ export class SettingsView {
} }
}); });
// Bible translation sub-list
document.querySelector('.offline-type[value="bible"]')?.addEventListener('change', async (e) => {
const checked = (e.target as HTMLInputElement).checked;
const container = document.getElementById('bible-translations');
if (!container) return;
container.classList.toggle('hidden', !checked);
if (!checked) { container.innerHTML = ''; return; }
try {
const resp = await fetch('/data/bible/versions.json');
const versions = await resp.json();
container.innerHTML = versions.map((v: any) => `
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" value="${v.id}" class="bible-trans accent-gold">
<span class="text-xs text-navy">${v.name}</span>
<span class="text-[10px] text-navy-light">(${OfflineManager.getBibleSize(v.id) || '?'})</span>
</label>
`).join('');
} catch {}
});
// Offline content - auto-select ru+cu when ru is selected // Offline content - auto-select ru+cu when ru is selected
document.querySelectorAll('.offline-lang').forEach(el => { document.querySelectorAll('.offline-lang').forEach(el => {
el.addEventListener('change', () => { el.addEventListener('change', () => {
@@ -438,8 +462,15 @@ export class SettingsView {
const progressText = document.getElementById('offline-progress-text'); const progressText = document.getElementById('offline-progress-text');
if (progressDiv) progressDiv.classList.remove('hidden'); if (progressDiv) progressDiv.classList.remove('hidden');
const bibleTrans = Array.from(document.querySelectorAll('.bible-trans:checked'))
.map(el => (el as HTMLInputElement).value);
// Start preloading in background // Start preloading in background
OfflineManager.preload({ languages: selectedLangs, types: selectedTypes }).then(() => { OfflineManager.preload({
languages: selectedLangs,
types: selectedTypes,
bibleTranslations: bibleTrans,
}).then(() => {
if (progressBar) progressBar.style.width = '100%'; if (progressBar) progressBar.style.width = '100%';
if (progressText) progressText.textContent = t.settings.offlineDone; if (progressText) progressText.textContent = t.settings.offlineDone;
OfflineManager.getStats().then(stats => { OfflineManager.getStats().then(stats => {