From 0769271f98c1c31ab1ca31051643455b6c8609ab 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: Sun, 2 Aug 2026 21:47:31 +0300 Subject: [PATCH] UX: comm chevron, lang fix, PWA cache control, Bible expand, app title --- src/core/i18n.ts | 14 ++++- src/ui/app.ts | 9 ++- src/ui/calendar-view.ts | 28 +++++---- src/ui/settings-view.ts | 123 +++++++++++++++++++++++++++++++++------- 4 files changed, 137 insertions(+), 37 deletions(-) diff --git a/src/core/i18n.ts b/src/core/i18n.ts index 801502b..6b22c17 100644 --- a/src/core/i18n.ts +++ b/src/core/i18n.ts @@ -28,6 +28,7 @@ export interface Translations { triodion: string; settings: string; library: string; + appTitle: string; festal: string; }; @@ -500,6 +501,7 @@ export interface Translations { offlineCalculating: string; offlineDone: string; offlineSelectLang: string; + offlineSelectAll: string; offlineCleared: string; offlineCacheInfo: string; locationTitle: string; @@ -529,7 +531,7 @@ const translations: Record = { nav: { calendar: '📅 Calendar', service: '📖 Service', - bible: '☦ Bible', + bible: 'Bible', prayer: '📿 Prayer Rule', akathists: '📜 Canons & Akathists', parimii: '📖 Paroemia', @@ -541,6 +543,7 @@ const translations: Record = { triodion: '📗 Lenten Triodion', settings: '⚙ Settings', library: '📚 Library', + appTitle: 'Ponomar', festal: '📗 Feasts', }, @@ -1048,6 +1051,7 @@ const translations: Record = { offlineCalculating: 'Calculating storage...', offlineDone: 'Done! Data cached for offline use.', offlineSelectLang: 'Please select at least one language.', + offlineSelectAll: 'Select all', offlineCleared: 'Cache cleared.', offlineCacheInfo: 'Cache: {0} / {1} ({2} files)', locationTitle: 'Location', @@ -1075,7 +1079,7 @@ const translations: Record = { nav: { calendar: '📅 Календарь', service: '📖 Устав', - bible: '☦ Библия', + bible: 'Библия', prayer: '📿 Молитвослов', akathists: '📜 Каноны и акафисты', parimii: '📖 Паримии', @@ -1087,6 +1091,7 @@ const translations: Record = { triodion: '📗 Триодь постная', settings: '⚙ Настройки', library: '📚 Библиотека', + appTitle: 'Пономарь', festal: '📗 Праздники', }, @@ -1594,6 +1599,7 @@ const translations: Record = { offlineCalculating: 'Расчёт размера...', offlineDone: 'Готово! Данные закэшированы для офлайн-использования.', offlineSelectLang: 'Пожалуйста, выберите хотя бы один язык.', + offlineSelectAll: 'Выбрать все', offlineCleared: 'Кэш очищен.', offlineCacheInfo: 'Кэш: {0} / {1} ({2} файлов)', locationTitle: 'Местоположение', @@ -1621,7 +1627,7 @@ const translations: Record = { nav: { calendar: '📅 Календарь', service: '📖 Уставъ', - bible: '☦ Библия', + bible: 'Библия', prayer: '📿 Молитвосло́въ', akathists: '📜 Кано́ны и҆ а҆ка́ѳїсты', parimii: '📖 Паремі́и', @@ -1633,6 +1639,7 @@ const translations: Record = { triodion: '📗 Трїѡ́дь по́стнаѧ', settings: '⚙ Настройки', library: '📚 Библіоте́ка', + appTitle: 'Понома́рь', festal: '📗 Пра́здники', }, @@ -2140,6 +2147,7 @@ const translations: Record = { offlineCalculating: 'Расчётъ размѣра...', offlineDone: 'Готово! Данныя закешированы для офлайн-потребленїя.', offlineSelectLang: 'Пожалуйста, изберите хотя бы единъ языкъ.', + offlineSelectAll: 'И҆збра́ти всѧ̑', offlineCleared: 'Кешъ очищенъ.', offlineCacheInfo: 'Кешъ: {0} / {1} ({2} фа́йлѡвъ)', locationTitle: 'Мѣстоположе́нїе', diff --git a/src/ui/app.ts b/src/ui/app.ts index 05a0787..793527c 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -114,16 +114,21 @@ export class App { } private render() { + this.settings = loadSettings(); const t = getTranslations(this.settings.language as LanguageCode); this.container.innerHTML = `
-
+
+
@@ -413,24 +415,50 @@ export class SettingsView { } }); - // Bible translation sub-list - document.querySelector('.offline-type[value="bible"]')?.addEventListener('change', async (e) => { - const checked = (e.target as HTMLInputElement).checked; + // Bible translations expand/collapse via Bible row + document.getElementById('bible-row')?.addEventListener('click', async () => { 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) => ` - - `).join(''); - } catch {} + const chevron = document.getElementById('bible-chevron'); + if (!container || !chevron) return; + const isHidden = container.classList.contains('hidden'); + if (isHidden) { + // Load translations on first expand + if (container.innerHTML === '') { + try { + const resp = await fetch('/data/bible/versions.json'); + const versions = await resp.json(); + const html = await Promise.all(versions.map(async (v: any) => { + const firstBook = v.books?.[0]?.id; + let isCached = false; + if (firstBook) { + isCached = (await DataCache.get(`url:/data/${v.id}/${firstBook}.text`)) !== null; + } + return ` + + `; + })); + container.innerHTML = ` +
+ +
+ ` + html.join(''); + document.getElementById('select-all-bible')?.addEventListener('click', () => { + document.querySelectorAll('.bible-trans').forEach(el => { + (el as HTMLInputElement).checked = true; + }); + }); + } catch {} + } + container.classList.remove('hidden'); + chevron.textContent = '▼'; + } else { + container.classList.add('hidden'); + chevron.textContent = '▶'; + } }); // Offline content - auto-select ru+cu when ru is selected @@ -452,6 +480,8 @@ export class SettingsView { .map(el => (el as HTMLInputElement).value) as ('lives' | 'bible' | 'calendar' | 'menaion')[]; if (selectedLangs.length === 0) { + const progressDiv = document.getElementById('offline-progress'); + if (progressDiv) progressDiv.classList.remove('hidden'); const progressText = document.getElementById('offline-progress-text'); if (progressText) progressText.textContent = t.settings.offlineSelectLang; return; @@ -516,6 +546,59 @@ export class SettingsView { } }); + // Check cache status for each data type and set checkbox states + (async () => { + const checkCache = async (key: string): Promise => { + return (await DataCache.get(key)) !== null; + }; + const setChecked = (selector: string, checked: boolean) => { + const el = document.querySelector(selector) as HTMLInputElement | null; + if (el) el.checked = checked; + }; + + // Check main types + setChecked('.offline-type[value="lives"]', await checkCache('url:/data/ru/lives/01.json')); + setChecked('.offline-type[value="calendar"]', await checkCache('url:/data/shared/fasting.json')); + setChecked('.offline-type[value="menaion"]', await checkCache('url:/data/en/menaion-bundle.json')); + + // Count cached Bible translations + (async () => { + try { + const resp = await fetch('/data/bible/versions.json'); + const versions = await resp.json(); + const total = versions.length; + let cached = 0; + for (const v of versions) { + const firstBook = v.books?.[0]?.id; + if (firstBook && await checkCache(`url:/data/${v.id}/${firstBook}.text`)) { + cached++; + } + } + const countEl = document.getElementById('bible-count'); + if (countEl) countEl.textContent = `(${cached}/${total})`; + } catch {} + })(); + + // Check language checkboxes based on cache status + for (const lang of ['en', 'ru', 'cu']) { + const isCached = await checkCache(`url:/data/${lang}/lives/01.json`); + setChecked(`.offline-lang[value="${lang}"]`, isCached); + } + + // If nothing cached, default to current interface language + const anyChecked = document.querySelector('.offline-lang:checked'); + if (!anyChecked) { + setChecked(`.offline-lang[value="${this.settings.language}"]`, true); + } + + // Auto-select cu when ru is checked + const ruChecked = (document.querySelector('.offline-lang[value="ru"]') as HTMLInputElement)?.checked; + const cuCheckbox = document.querySelector('.offline-lang[value="cu"]') as HTMLInputElement; + if (ruChecked && cuCheckbox) { + cuCheckbox.checked = true; + } + })(); + this.updateFontPreview(); }