UX: comm chevron, lang fix, PWA cache control, Bible expand, app title
This commit is contained in:
1 parent
cb812fe4fe
commit
0769271f98
4 files changed
+121
-21
No files matched your search
+11
-3
@@ -28,6 +28,7 @@ export interface Translations {
|
|||||||
triodion: string;
|
triodion: string;
|
||||||
settings: string;
|
settings: string;
|
||||||
library: string;
|
library: string;
|
||||||
|
appTitle: string;
|
||||||
festal: string;
|
festal: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -500,6 +501,7 @@ export interface Translations {
|
|||||||
offlineCalculating: string;
|
offlineCalculating: string;
|
||||||
offlineDone: string;
|
offlineDone: string;
|
||||||
offlineSelectLang: string;
|
offlineSelectLang: string;
|
||||||
|
offlineSelectAll: string;
|
||||||
offlineCleared: string;
|
offlineCleared: string;
|
||||||
offlineCacheInfo: string;
|
offlineCacheInfo: string;
|
||||||
locationTitle: string;
|
locationTitle: string;
|
||||||
@@ -529,7 +531,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
nav: {
|
nav: {
|
||||||
calendar: '📅 Calendar',
|
calendar: '📅 Calendar',
|
||||||
service: '📖 Service',
|
service: '📖 Service',
|
||||||
bible: '☦ Bible',
|
bible: 'Bible',
|
||||||
prayer: '📿 Prayer Rule',
|
prayer: '📿 Prayer Rule',
|
||||||
akathists: '📜 Canons & Akathists',
|
akathists: '📜 Canons & Akathists',
|
||||||
parimii: '📖 Paroemia',
|
parimii: '📖 Paroemia',
|
||||||
@@ -541,6 +543,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
triodion: '📗 Lenten Triodion',
|
triodion: '📗 Lenten Triodion',
|
||||||
settings: '⚙ Settings',
|
settings: '⚙ Settings',
|
||||||
library: '📚 Library',
|
library: '📚 Library',
|
||||||
|
appTitle: 'Ponomar',
|
||||||
festal: '📗 Feasts',
|
festal: '📗 Feasts',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1048,6 +1051,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
offlineCalculating: 'Calculating storage...',
|
offlineCalculating: 'Calculating storage...',
|
||||||
offlineDone: 'Done! Data cached for offline use.',
|
offlineDone: 'Done! Data cached for offline use.',
|
||||||
offlineSelectLang: 'Please select at least one language.',
|
offlineSelectLang: 'Please select at least one language.',
|
||||||
|
offlineSelectAll: 'Select all',
|
||||||
offlineCleared: 'Cache cleared.',
|
offlineCleared: 'Cache cleared.',
|
||||||
offlineCacheInfo: 'Cache: {0} / {1} ({2} files)',
|
offlineCacheInfo: 'Cache: {0} / {1} ({2} files)',
|
||||||
locationTitle: 'Location',
|
locationTitle: 'Location',
|
||||||
@@ -1075,7 +1079,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
nav: {
|
nav: {
|
||||||
calendar: '📅 Календарь',
|
calendar: '📅 Календарь',
|
||||||
service: '📖 Устав',
|
service: '📖 Устав',
|
||||||
bible: '☦ Библия',
|
bible: 'Библия',
|
||||||
prayer: '📿 Молитвослов',
|
prayer: '📿 Молитвослов',
|
||||||
akathists: '📜 Каноны и акафисты',
|
akathists: '📜 Каноны и акафисты',
|
||||||
parimii: '📖 Паримии',
|
parimii: '📖 Паримии',
|
||||||
@@ -1087,6 +1091,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
triodion: '📗 Триодь постная',
|
triodion: '📗 Триодь постная',
|
||||||
settings: '⚙ Настройки',
|
settings: '⚙ Настройки',
|
||||||
library: '📚 Библиотека',
|
library: '📚 Библиотека',
|
||||||
|
appTitle: 'Пономарь',
|
||||||
festal: '📗 Праздники',
|
festal: '📗 Праздники',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1594,6 +1599,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
offlineCalculating: 'Расчёт размера...',
|
offlineCalculating: 'Расчёт размера...',
|
||||||
offlineDone: 'Готово! Данные закэшированы для офлайн-использования.',
|
offlineDone: 'Готово! Данные закэшированы для офлайн-использования.',
|
||||||
offlineSelectLang: 'Пожалуйста, выберите хотя бы один язык.',
|
offlineSelectLang: 'Пожалуйста, выберите хотя бы один язык.',
|
||||||
|
offlineSelectAll: 'Выбрать все',
|
||||||
offlineCleared: 'Кэш очищен.',
|
offlineCleared: 'Кэш очищен.',
|
||||||
offlineCacheInfo: 'Кэш: {0} / {1} ({2} файлов)',
|
offlineCacheInfo: 'Кэш: {0} / {1} ({2} файлов)',
|
||||||
locationTitle: 'Местоположение',
|
locationTitle: 'Местоположение',
|
||||||
@@ -1621,7 +1627,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
nav: {
|
nav: {
|
||||||
calendar: '📅 Календарь',
|
calendar: '📅 Календарь',
|
||||||
service: '📖 Уставъ',
|
service: '📖 Уставъ',
|
||||||
bible: '☦ Библия',
|
bible: 'Библия',
|
||||||
prayer: '📿 Молитвосло́въ',
|
prayer: '📿 Молитвосло́въ',
|
||||||
akathists: '📜 Кано́ны и҆ а҆ка́ѳїсты',
|
akathists: '📜 Кано́ны и҆ а҆ка́ѳїсты',
|
||||||
parimii: '📖 Паремі́и',
|
parimii: '📖 Паремі́и',
|
||||||
@@ -1633,6 +1639,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
triodion: '📗 Трїѡ́дь по́стнаѧ',
|
triodion: '📗 Трїѡ́дь по́стнаѧ',
|
||||||
settings: '⚙ Настройки',
|
settings: '⚙ Настройки',
|
||||||
library: '📚 Библіоте́ка',
|
library: '📚 Библіоте́ка',
|
||||||
|
appTitle: 'Понома́рь',
|
||||||
festal: '📗 Пра́здники',
|
festal: '📗 Пра́здники',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2140,6 +2147,7 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
offlineCalculating: 'Расчётъ размѣра...',
|
offlineCalculating: 'Расчётъ размѣра...',
|
||||||
offlineDone: 'Готово! Данныя закешированы для офлайн-потребленїя.',
|
offlineDone: 'Готово! Данныя закешированы для офлайн-потребленїя.',
|
||||||
offlineSelectLang: 'Пожалуйста, изберите хотя бы единъ языкъ.',
|
offlineSelectLang: 'Пожалуйста, изберите хотя бы единъ языкъ.',
|
||||||
|
offlineSelectAll: 'И҆збра́ти всѧ̑',
|
||||||
offlineCleared: 'Кешъ очищенъ.',
|
offlineCleared: 'Кешъ очищенъ.',
|
||||||
offlineCacheInfo: 'Кешъ: {0} / {1} ({2} фа́йлѡвъ)',
|
offlineCacheInfo: 'Кешъ: {0} / {1} ({2} фа́йлѡвъ)',
|
||||||
locationTitle: 'Мѣстоположе́нїе',
|
locationTitle: 'Мѣстоположе́нїе',
|
||||||
|
|||||||
+7
-2
@@ -114,16 +114,21 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private render() {
|
private render() {
|
||||||
|
this.settings = loadSettings();
|
||||||
const t = getTranslations(this.settings.language as LanguageCode);
|
const t = getTranslations(this.settings.language as LanguageCode);
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
<div class="app-container min-h-screen flex flex-col">
|
<div class="app-container min-h-screen flex flex-col">
|
||||||
<header class="bg-navy text-parchment p-3 flex items-center shadow-md">
|
<header class="bg-navy text-parchment p-3 flex items-center shadow-md relative">
|
||||||
|
<span id="app-title" class="hidden md:inline-flex items-center gap-2 text-lg font-bold whitespace-nowrap absolute left-3 top-1/2 -translate-y-1/2">
|
||||||
|
<img src="/icons/icon-192.png" class="w-6 h-6" alt="">
|
||||||
|
${t.nav.appTitle}
|
||||||
|
</span>
|
||||||
<nav class="flex flex-wrap gap-4 text-sm mx-auto">
|
<nav class="flex flex-wrap gap-4 text-sm mx-auto">
|
||||||
<a href="#calendar" class="nav-link hover:text-gold transition-colors" data-view="calendar">${t.nav.calendar}</a>
|
<a href="#calendar" class="nav-link hover:text-gold transition-colors" data-view="calendar">${t.nav.calendar}</a>
|
||||||
<details class="nav-group relative" data-nav="library">
|
<details class="nav-group relative" data-nav="library">
|
||||||
<summary class="cursor-pointer list-none hover:text-gold transition-colors flex items-center gap-1">${t.nav.library} <span class="text-xs">▾</span></summary>
|
<summary class="cursor-pointer list-none hover:text-gold transition-colors flex items-center gap-1">${t.nav.library} <span class="text-xs">▾</span></summary>
|
||||||
<div class="absolute left-0 top-full mt-1 z-50 min-w-56 rounded-lg bg-navy-light shadow-xl p-2 flex flex-col gap-1">
|
<div class="absolute left-0 top-full mt-1 z-50 min-w-56 rounded-lg bg-navy-light shadow-xl p-2 flex flex-col gap-1">
|
||||||
<a href="#bible" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="bible">${t.nav.bible}</a>
|
<a href="#bible" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="bible"><span>📖</span> <span class="font-bold">${t.nav.bible}</span></a>
|
||||||
<a href="#prayer" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="prayer">${t.nav.prayer}</a>
|
<a href="#prayer" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="prayer">${t.nav.prayer}</a>
|
||||||
<a href="#akathists" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="akathists">${t.nav.akathists}</a>
|
<a href="#akathists" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="akathists">${t.nav.akathists}</a>
|
||||||
<a href="#horologion" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="horologion">${t.nav.horologion}</a>
|
<a href="#horologion" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="horologion">${t.nav.horologion}</a>
|
||||||
|
|||||||
@@ -798,8 +798,8 @@ export class CalendarView {
|
|||||||
? `<div class="life-text hidden mt-2 p-3 bg-white border border-gold/20 rounded text-sm leading-relaxed max-h-60 overflow-y-auto">${lifeText}</div>`
|
? `<div class="life-text hidden mt-2 p-3 bg-white border border-gold/20 rounded text-sm leading-relaxed max-h-60 overflow-y-auto">${lifeText}</div>`
|
||||||
: '';
|
: '';
|
||||||
commHtmlParts.push(
|
commHtmlParts.push(
|
||||||
`<div class="comm-item">
|
`<div class="comm-item">
|
||||||
<div class="text-sm font-medium ${lifeText ? 'text-blue-600 underline cursor-pointer hover:text-blue-800' : 'text-navy'}">${iconHtml}${name}</div>
|
<div class="text-sm font-medium ${lifeText ? 'text-blue-600 underline cursor-pointer hover:text-blue-800' : 'text-navy'}">${iconHtml}${name}${lifeText ? '<span class="comm-chevron text-xs align-middle ml-1 inline-block">▶</span>' : ''}</div>
|
||||||
${lifeHtml}
|
${lifeHtml}
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
@@ -810,12 +810,16 @@ export class CalendarView {
|
|||||||
<h3 class="font-bold text-red mb-2">${this.t.calendar.commemorations}</h3>
|
<h3 class="font-bold text-red mb-2">${this.t.calendar.commemorations}</h3>
|
||||||
<div class="space-y-1">${commHtmlParts.join('')}</div>
|
<div class="space-y-1">${commHtmlParts.join('')}</div>
|
||||||
`;
|
`;
|
||||||
commPanel.addEventListener('click', (e) => {
|
commPanel.addEventListener('click', (e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const item = target.closest('.comm-item');
|
const item = target.closest('.comm-item');
|
||||||
if (item) {
|
if (item) {
|
||||||
const lifeDiv = item.querySelector('.life-text') as HTMLElement;
|
const lifeDiv = item.querySelector('.life-text') as HTMLElement;
|
||||||
if (lifeDiv) lifeDiv.classList.toggle('hidden');
|
if (lifeDiv) {
|
||||||
|
lifeDiv.classList.toggle('hidden');
|
||||||
|
const chevron = item.querySelector('.comm-chevron') as HTMLElement;
|
||||||
|
if (chevron) chevron.textContent = lifeDiv.classList.contains('hidden') ? '▶' : '▼';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+95
-12
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { getTranslations, type LanguageCode } from '../core/i18n';
|
import { getTranslations, type LanguageCode } from '../core/i18n';
|
||||||
import { OfflineManager } from '../core/offline-manager';
|
import { OfflineManager } from '../core/offline-manager';
|
||||||
|
import { DataCache } from '../core/data-cache';
|
||||||
|
|
||||||
const LANGUAGES = [
|
const LANGUAGES = [
|
||||||
{ code: 'en', name: 'English', local: 'English' },
|
{ code: 'en', name: 'English', local: 'English' },
|
||||||
@@ -292,9 +293,10 @@ 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">
|
<div id="bible-row" 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>
|
<span class="text-sm">${t.settings.offlineBible} <span id="bible-count" class="text-xs text-navy-light">(0/0)</span></span>
|
||||||
</label>
|
<span id="bible-chevron" class="text-xs text-navy-light hover:text-navy select-none">▶</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="bible-translations" class="hidden ml-4 mt-1 space-y-1 border-l border-gold/20 pl-3"></div>
|
<div id="bible-translations" class="hidden ml-4 mt-1 space-y-1 border-l border-gold/20 pl-3"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -413,24 +415,50 @@ export class SettingsView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bible translation sub-list
|
// Bible translations expand/collapse via Bible row
|
||||||
document.querySelector('.offline-type[value="bible"]')?.addEventListener('change', async (e) => {
|
document.getElementById('bible-row')?.addEventListener('click', async () => {
|
||||||
const checked = (e.target as HTMLInputElement).checked;
|
|
||||||
const container = document.getElementById('bible-translations');
|
const container = document.getElementById('bible-translations');
|
||||||
if (!container) return;
|
const chevron = document.getElementById('bible-chevron');
|
||||||
container.classList.toggle('hidden', !checked);
|
if (!container || !chevron) return;
|
||||||
if (!checked) { container.innerHTML = ''; return; }
|
const isHidden = container.classList.contains('hidden');
|
||||||
|
if (isHidden) {
|
||||||
|
// Load translations on first expand
|
||||||
|
if (container.innerHTML === '') {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/bible/versions.json');
|
const resp = await fetch('/data/bible/versions.json');
|
||||||
const versions = await resp.json();
|
const versions = await resp.json();
|
||||||
container.innerHTML = versions.map((v: any) => `
|
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 `
|
||||||
<label class="flex items-center gap-1 cursor-pointer">
|
<label class="flex items-center gap-1 cursor-pointer">
|
||||||
<input type="checkbox" value="${v.id}" class="bible-trans accent-gold">
|
<input type="checkbox" value="${v.id}" class="bible-trans accent-gold" ${isCached ? 'checked' : ''}>
|
||||||
<span class="text-xs text-navy">${v.name}</span>
|
<span class="text-xs text-navy">${v.name}</span>
|
||||||
<span class="text-[10px] text-navy-light">(${OfflineManager.getBibleSize(v.id) || '?'})</span>
|
<span class="text-[10px] text-navy-light">(${OfflineManager.getBibleSize(v.id) || '?'})</span>
|
||||||
</label>
|
</label>
|
||||||
`).join('');
|
`;
|
||||||
|
}));
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="mb-1">
|
||||||
|
<button id="select-all-bible" class="text-xs text-blue-600 underline hover:text-blue-800 cursor-pointer">${t.settings.offlineSelectAll}</button>
|
||||||
|
</div>
|
||||||
|
` + html.join('');
|
||||||
|
document.getElementById('select-all-bible')?.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.bible-trans').forEach(el => {
|
||||||
|
(el as HTMLInputElement).checked = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
}
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
chevron.textContent = '▼';
|
||||||
|
} else {
|
||||||
|
container.classList.add('hidden');
|
||||||
|
chevron.textContent = '▶';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Offline content - auto-select ru+cu when ru is selected
|
// 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')[];
|
.map(el => (el as HTMLInputElement).value) as ('lives' | 'bible' | 'calendar' | 'menaion')[];
|
||||||
|
|
||||||
if (selectedLangs.length === 0) {
|
if (selectedLangs.length === 0) {
|
||||||
|
const progressDiv = document.getElementById('offline-progress');
|
||||||
|
if (progressDiv) progressDiv.classList.remove('hidden');
|
||||||
const progressText = document.getElementById('offline-progress-text');
|
const progressText = document.getElementById('offline-progress-text');
|
||||||
if (progressText) progressText.textContent = t.settings.offlineSelectLang;
|
if (progressText) progressText.textContent = t.settings.offlineSelectLang;
|
||||||
return;
|
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<boolean> => {
|
||||||
|
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();
|
this.updateFontPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user