feat: serve all precached data offline via DataCache fallback
- DataCache.fetchWithFallback: network-first with IDB write-through under
the same url: keys the preload writes; falls back to precached IDB copy
when offline — closes the preload(IDB) vs read(SW cache) gap that made
the Bible reader (and every other view) fail offline despite precaching
- Convert all 33 raw /data fetches across bible/service/menaion/prayer/
calendar/settings views; in-memory caches and notFound maps preserved
- fetchCommandText: switch to lowercase services/commands/ (already in
preload manifest); tolerate both TEXT/text node casings per migrate-data
- Remove stale duplicate static/data/en/services/Command/ (15 files)
- fetchLives: fix misc fallback to real lives/misc/{N}.json bundles
This commit is contained in:
1 parent
623c56a96f
commit
a005e386a6
22 files changed
+100
-180
No files matched your search
@@ -204,6 +204,30 @@ export class DataCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network-first fetch with IndexedDB fallback.
|
||||||
|
*
|
||||||
|
* Online: fetches, parses and writes through to IDB under the same
|
||||||
|
* `url:` keys the offline preload uses. Offline: serves the precached
|
||||||
|
* IDB copy, or null when the file was never preloaded.
|
||||||
|
*/
|
||||||
|
static async fetchWithFallback<T = unknown>(url: string, type: 'json' | 'text' = 'json'): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
const ct = resp.headers.get('content-type');
|
||||||
|
if (resp.ok && !(ct && ct.includes('text/html'))) {
|
||||||
|
const data: unknown = type === 'text' ? await resp.text() : await resp.json();
|
||||||
|
await DataCache.set(`url:${url}`, data);
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
} catch { /* offline or invalid response — fall through to IDB */ }
|
||||||
|
try {
|
||||||
|
return ((await DataCache.get(`url:${url}`)) as T) ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache-first fetch: tries IndexedDB first, falls back to network.
|
* Cache-first fetch: tries IndexedDB first, falls back to network.
|
||||||
* If the data is fetched from network, it's cached for future use.
|
* If the data is fetched from network, it's cached for future use.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
formatPassageHTML,
|
formatPassageHTML,
|
||||||
} from '../core/bible-reader';
|
} from '../core/bible-reader';
|
||||||
import { getTranslations, type LanguageCode } from '../core/i18n';
|
import { getTranslations, type LanguageCode } from '../core/i18n';
|
||||||
|
import { DataCache } from '../core/data-cache';
|
||||||
|
|
||||||
const SETTINGS_KEY = 'ponomar-settings';
|
const SETTINGS_KEY = 'ponomar-settings';
|
||||||
const BIBLE_STATE_KEY = 'ponomar-bible-state';
|
const BIBLE_STATE_KEY = 'ponomar-bible-state';
|
||||||
@@ -138,8 +139,9 @@ export class BibleView {
|
|||||||
|
|
||||||
private async loadVersions() {
|
private async loadVersions() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/bible/versions.json');
|
const versions = await DataCache.fetchWithFallback<typeof this.versions>('/data/bible/versions.json');
|
||||||
this.versions = await resp.json();
|
if (!versions || !Array.isArray(versions) || versions.length === 0) throw new Error('unavailable');
|
||||||
|
this.versions = versions;
|
||||||
this.sortVersionsByLanguage();
|
this.sortVersionsByLanguage();
|
||||||
} catch {
|
} catch {
|
||||||
this.versions = [
|
this.versions = [
|
||||||
@@ -473,9 +475,8 @@ export class BibleView {
|
|||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
// Version IDs are full paths like "en/bible/kjv"
|
// Version IDs are full paths like "en/bible/kjv"
|
||||||
const resp = await fetch(`/data/${this.currentVersion}/${this.currentBook}.text`);
|
text = (await DataCache.fetchWithFallback<string>(`/data/${this.currentVersion}/${this.currentBook}.text`, 'text')) ?? undefined;
|
||||||
if (!resp.ok) throw new Error('Not found');
|
if (!text) throw new Error('Not found');
|
||||||
text = await resp.text();
|
|
||||||
this.loadedText.set(cacheKey, text);
|
this.loadedText.set(cacheKey, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-24
@@ -52,12 +52,10 @@ async function loadBookAbbrevs(lang: string): Promise<void> {
|
|||||||
bookAbbrevLang = lang;
|
bookAbbrevLang = lang;
|
||||||
for (let attempt = 0; attempt < 2; attempt++) {
|
for (let attempt = 0; attempt < 2; attempt++) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/bible/versions.json');
|
const versions = await DataCache.fetchWithFallback<any[]>('/data/bible/versions.json');
|
||||||
if (!resp.ok) { if (attempt === 0) await new Promise(r => setTimeout(r, 300)); continue; }
|
const ver = Array.isArray(versions)
|
||||||
const ct = resp.headers.get('content-type');
|
? (versions.find((v: any) => v.id.startsWith(lang + '/')) || versions[0])
|
||||||
if (ct && ct.includes('text/html')) { if (attempt === 0) await new Promise(r => setTimeout(r, 300)); continue; }
|
: null;
|
||||||
const versions = await resp.json() as any[];
|
|
||||||
const ver = versions.find((v: any) => v.id.startsWith(lang + '/')) || versions[0];
|
|
||||||
if (ver) {
|
if (ver) {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
if (ver.books) {
|
if (ver.books) {
|
||||||
@@ -72,8 +70,9 @@ async function loadBookAbbrevs(lang: string): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (attempt === 0) await new Promise(r => setTimeout(r, 300));
|
// fall through
|
||||||
}
|
}
|
||||||
|
if (attempt === 0) await new Promise(r => setTimeout(r, 300));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,11 +294,7 @@ async function loadLife(lang: LanguageCode, cid: string): Promise<LifeData | nul
|
|||||||
|
|
||||||
async function loadCommemoration(sharedId: string): Promise<CommemorationData | null> {
|
async function loadCommemoration(sharedId: string): Promise<CommemorationData | null> {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/data/shared/commemorations/${sharedId}.json`);
|
return await DataCache.fetchWithFallback<CommemorationData>(`/data/shared/commemorations/${sharedId}.json`);
|
||||||
if (!resp.ok) return null;
|
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) return null;
|
|
||||||
return await resp.json();
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -350,12 +345,8 @@ async function loadMenaion(lang: LanguageCode, month: number, day: number): Prom
|
|||||||
async function loadTriodionData(): Promise<Record<string, unknown>> {
|
async function loadTriodionData(): Promise<Record<string, unknown>> {
|
||||||
if (cachedTriodion) return cachedTriodion;
|
if (cachedTriodion) return cachedTriodion;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/shared/calendar/triodion.json');
|
cachedTriodion = (await DataCache.fetchWithFallback<Record<string, unknown>>('/data/shared/calendar/triodion.json')) ?? {};
|
||||||
if (!resp.ok) return {};
|
return cachedTriodion;
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) return {};
|
|
||||||
cachedTriodion = await resp.json();
|
|
||||||
return cachedTriodion!;
|
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -371,12 +362,8 @@ async function loadTriodionFile(index: number): Promise<unknown[]> {
|
|||||||
async function loadPentecostarionData(): Promise<Record<string, unknown>> {
|
async function loadPentecostarionData(): Promise<Record<string, unknown>> {
|
||||||
if (cachedPentecostarion) return cachedPentecostarion;
|
if (cachedPentecostarion) return cachedPentecostarion;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/shared/calendar/pentecostarion.json');
|
cachedPentecostarion = (await DataCache.fetchWithFallback<Record<string, unknown>>('/data/shared/calendar/pentecostarion.json')) ?? {};
|
||||||
if (!resp.ok) return {};
|
return cachedPentecostarion;
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) return {};
|
|
||||||
cachedPentecostarion = await resp.json();
|
|
||||||
return cachedPentecostarion!;
|
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { assembleService, type ServiceContext } from '../core/service-assembler'
|
|||||||
import type { EvalContext, ServiceNode } from '../core/types';
|
import type { EvalContext, ServiceNode } from '../core/types';
|
||||||
import { getTranslations, type LanguageCode } from '../core/i18n';
|
import { getTranslations, type LanguageCode } from '../core/i18n';
|
||||||
import { loadSettings, fontClass } from './settings-view';
|
import { loadSettings, fontClass } from './settings-view';
|
||||||
|
import { DataCache } from '../core/data-cache';
|
||||||
|
|
||||||
export class MenaionView {
|
export class MenaionView {
|
||||||
private container: HTMLElement;
|
private container: HTMLElement;
|
||||||
@@ -45,9 +46,9 @@ export class MenaionView {
|
|||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/shared/menaion-daily/index.json');
|
const index = await DataCache.fetchWithFallback<Record<string, number>>('/data/shared/menaion-daily/index.json');
|
||||||
if (resp.ok) {
|
if (index) {
|
||||||
this.index = await resp.json() as Record<string, number>;
|
this.index = index;
|
||||||
}
|
}
|
||||||
} catch { /* empty */ }
|
} catch { /* empty */ }
|
||||||
|
|
||||||
@@ -164,9 +165,8 @@ export class MenaionView {
|
|||||||
let html = '';
|
let html = '';
|
||||||
for (let i = 1; i <= count; i++) {
|
for (let i = 1; i <= count; i++) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/data/shared/menaion-daily/${dateKey}/${i}.json`);
|
const nodes = await DataCache.fetchWithFallback<ServiceNode[]>(`/data/shared/menaion-daily/${dateKey}/${i}.json`);
|
||||||
if (!resp.ok) continue;
|
if (!nodes) continue;
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
const evalCtx: EvalContext = {};
|
const evalCtx: EvalContext = {};
|
||||||
const ctx: ServiceContext = {
|
const ctx: ServiceContext = {
|
||||||
evalCtx, lang: 'cu', t: this.t,
|
evalCtx, lang: 'cu', t: this.t,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { assembleService, type ServiceContext } from '../core/service-assembler'
|
|||||||
import type { EvalContext, ServiceNode } from '../core/types';
|
import type { EvalContext, ServiceNode } from '../core/types';
|
||||||
import { getTranslations, type LanguageCode } from '../core/i18n';
|
import { getTranslations, type LanguageCode } from '../core/i18n';
|
||||||
import { loadSettings, fontClass } from './settings-view';
|
import { loadSettings, fontClass } from './settings-view';
|
||||||
|
import { DataCache } from '../core/data-cache';
|
||||||
|
|
||||||
export type PrayerCollection = 'prayer-rule' | 'akathists' | 'parimii' | 'horologion' | 'sbornik' | 'paraclete' | 'irmologion' | 'triodion';
|
export type PrayerCollection = 'prayer-rule' | 'akathists' | 'parimii' | 'horologion' | 'sbornik' | 'paraclete' | 'irmologion' | 'triodion';
|
||||||
|
|
||||||
@@ -270,12 +271,11 @@ export class PrayerView {
|
|||||||
}
|
}
|
||||||
dataPath = `/data/shared/services/paraclete/tone${m[1]}/${m[2]}.json`;
|
dataPath = `/data/shared/services/paraclete/tone${m[1]}/${m[2]}.json`;
|
||||||
}
|
}
|
||||||
const resp = await fetch(dataPath);
|
const nodes = await DataCache.fetchWithFallback<ServiceNode[]>(dataPath);
|
||||||
if (!resp.ok) {
|
if (!nodes) {
|
||||||
contentEl.innerHTML = `<p class="text-navy-light">${this.t.prayer.notFound}</p>`;
|
contentEl.innerHTML = `<p class="text-navy-light">${this.t.prayer.notFound}</p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
|
|
||||||
const evalCtx: EvalContext = {};
|
const evalCtx: EvalContext = {};
|
||||||
const ctx: ServiceContext = {
|
const ctx: ServiceContext = {
|
||||||
|
|||||||
+44
-121
@@ -15,6 +15,7 @@ import {
|
|||||||
HOLY_WEEK, holyWeekVarName, type ServiceTabDef,
|
HOLY_WEEK, holyWeekVarName, type ServiceTabDef,
|
||||||
} from '../core/service-tabs';
|
} from '../core/service-tabs';
|
||||||
import { loadSettings, fontClass } from './settings-view';
|
import { loadSettings, fontClass } from './settings-view';
|
||||||
|
import { DataCache } from '../core/data-cache';
|
||||||
|
|
||||||
interface ServiceDef {
|
interface ServiceDef {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -74,9 +75,8 @@ export class ServiceView {
|
|||||||
|
|
||||||
// Load the daily Menaion index (date -> section count) for the Служба дня tabs
|
// Load the daily Menaion index (date -> section count) for the Служба дня tabs
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/shared/menaion-daily/index.json');
|
const index = await DataCache.fetchWithFallback<Record<string, number>>('/data/shared/menaion-daily/index.json');
|
||||||
if (resp.ok) {
|
if (index) {
|
||||||
const index = await resp.json() as Record<string, number>;
|
|
||||||
for (const [dk, n] of Object.entries(index)) {
|
for (const [dk, n] of Object.entries(index)) {
|
||||||
this.menaionDayCount.set(dk, n);
|
this.menaionDayCount.set(dk, n);
|
||||||
}
|
}
|
||||||
@@ -386,9 +386,8 @@ export class ServiceView {
|
|||||||
try {
|
try {
|
||||||
const tryFetch = async (lang: string) => {
|
const tryFetch = async (lang: string) => {
|
||||||
const url = `/data/${lang}/services/octoecheos/tone${toneNum}/${weekday.toLowerCase()}.json`;
|
const url = `/data/${lang}/services/octoecheos/tone${toneNum}/${weekday.toLowerCase()}.json`;
|
||||||
const resp = await fetch(url);
|
const nodes = await DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
if (!resp.ok) return null;
|
if (!nodes) return null;
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.type === serviceType) {
|
if (node.type === serviceType) {
|
||||||
if (!node.cmd || evalBool(node.cmd as string, this.evalContext)) {
|
if (!node.cmd || evalBool(node.cmd as string, this.evalContext)) {
|
||||||
@@ -417,11 +416,10 @@ export class ServiceView {
|
|||||||
try {
|
try {
|
||||||
const tryLoad = async (url: string): Promise<{ cid: string }[] | null> => {
|
const tryLoad = async (url: string): Promise<{ cid: string }[] | null> => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url);
|
const all = await DataCache.fetchWithFallback<Record<string, unknown>[]>(url);
|
||||||
if (!resp.ok) return null;
|
if (!all) return null;
|
||||||
const all = await resp.json();
|
|
||||||
if (Array.isArray(all)) return all.map((e: Record<string, unknown>) => ({ cid: String(e.id ?? '') }));
|
if (Array.isArray(all)) return all.map((e: Record<string, unknown>) => ({ cid: String(e.id ?? '') }));
|
||||||
const entries = all[dateKey] as Record<string, unknown>[] | undefined;
|
const entries = (all as Record<string, unknown>)[dateKey] as Record<string, unknown>[] | undefined;
|
||||||
if (Array.isArray(entries)) return entries.map((e: Record<string, unknown>) => ({ cid: String(e.id ?? '') }));
|
if (Array.isArray(entries)) return entries.map((e: Record<string, unknown>) => ({ cid: String(e.id ?? '') }));
|
||||||
return null;
|
return null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
@@ -435,11 +433,10 @@ export class ServiceView {
|
|||||||
// Load from merged calendar files
|
// Load from merged calendar files
|
||||||
const loadCalendarEntry = async (file: string, index: number): Promise<{ cid: string }[] | null> => {
|
const loadCalendarEntry = async (file: string, index: number): Promise<{ cid: string }[] | null> => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/data/shared/calendar/${file}.json`);
|
const data = await DataCache.fetchWithFallback<Record<string, unknown>>(`/data/shared/calendar/${file}.json`);
|
||||||
if (!resp.ok) return null;
|
if (!data) return null;
|
||||||
const data = await resp.json();
|
|
||||||
const key = String(index).padStart(2, '0');
|
const key = String(index).padStart(2, '0');
|
||||||
const entry = data[key];
|
const entry = data[key] as { id?: unknown } | undefined;
|
||||||
return entry ? [{ cid: String(entry.id ?? '') }] : null;
|
return entry ? [{ cid: String(entry.id ?? '') }] : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
};
|
};
|
||||||
@@ -457,9 +454,8 @@ export class ServiceView {
|
|||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
if (!entry.cid) continue;
|
if (!entry.cid) continue;
|
||||||
try {
|
try {
|
||||||
const shmResp = await fetch(`/data/shared/commemorations/${entry.cid}.json`);
|
const shm = await DataCache.fetchWithFallback<Record<string, unknown>>(`/data/shared/commemorations/${entry.cid}.json`);
|
||||||
if (!shmResp.ok) continue;
|
if (!shm) continue;
|
||||||
const shm = await shmResp.json();
|
|
||||||
const troparion = shm.TROPARION as { Tone?: string; text?: string } | undefined;
|
const troparion = shm.TROPARION as { Tone?: string; text?: string } | undefined;
|
||||||
const kontakion = shm.KONTAKION as { Tone?: string; text?: string } | undefined;
|
const kontakion = shm.KONTAKION as { Tone?: string; text?: string } | undefined;
|
||||||
if (troparion?.text || kontakion?.text) {
|
if (troparion?.text || kontakion?.text) {
|
||||||
@@ -507,9 +503,8 @@ export class ServiceView {
|
|||||||
const toneNum = tone === 8 ? 0 : tone;
|
const toneNum = tone === 8 ? 0 : tone;
|
||||||
const url = `/data/shared/services/canons/tone${toneNum}/sunday.json`;
|
const url = `/data/shared/services/canons/tone${toneNum}/sunday.json`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url);
|
const canonData = await DataCache.fetchWithFallback<CanonData>(url);
|
||||||
if (!resp.ok) return null;
|
if (!canonData) return null;
|
||||||
const canonData: CanonData = await resp.json();
|
|
||||||
const ODE_SLAV = ['', 'а', 'в', 'г', 'д', 'є', 'ѕ', 'з', 'и', 'ѳ'];
|
const ODE_SLAV = ['', 'а', 'в', 'г', 'д', 'є', 'ѕ', 'з', 'и', 'ѳ'];
|
||||||
|
|
||||||
const nodes: ServiceNode[] = [];
|
const nodes: ServiceNode[] = [];
|
||||||
@@ -543,62 +538,27 @@ export class ServiceView {
|
|||||||
const dayKey = WEEKDAY[dow] ?? '';
|
const dayKey = WEEKDAY[dow] ?? '';
|
||||||
if (!dayKey) return null;
|
if (!dayKey) return null;
|
||||||
const url = `/data/shared/services/canons/paraclete/tone${tone}/${dayKey}.json`;
|
const url = `/data/shared/services/canons/paraclete/tone${tone}/${dayKey}.json`;
|
||||||
try {
|
return DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) return null;
|
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
return nodes;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMenaionDay(dateKey: string, index: number): Promise<ServiceNode[] | null> {
|
private async loadMenaionDay(dateKey: string, index: number): Promise<ServiceNode[] | null> {
|
||||||
const url = `/data/shared/menaion-daily/${dateKey}/${index}.json`;
|
const url = `/data/shared/menaion-daily/${dateKey}/${index}.json`;
|
||||||
try {
|
return DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) return null;
|
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
return nodes;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadTriodion(index: number): Promise<ServiceNode[] | null> {
|
private async loadTriodion(index: number): Promise<ServiceNode[] | null> {
|
||||||
const url = `/data/shared/triodion/${String(index).padStart(2, '0')}.json`;
|
const url = `/data/shared/triodion/${String(index).padStart(2, '0')}.json`;
|
||||||
try {
|
return DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) return null;
|
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
return nodes;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMariasStandingData(): Promise<ServiceNode[] | null> {
|
private async loadMariasStandingData(): Promise<ServiceNode[] | null> {
|
||||||
const url = `/data/shared/services/marias-standing/full.json`;
|
const url = `/data/shared/services/marias-standing/full.json`;
|
||||||
try {
|
return DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) return null;
|
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
return nodes;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadHolyWeekData(service: string): Promise<ServiceNode[] | null> {
|
private async loadHolyWeekData(service: string): Promise<ServiceNode[] | null> {
|
||||||
const url = `/data/shared/services/${service}/full.json`;
|
const url = `/data/shared/services/${service}/full.json`;
|
||||||
try {
|
return DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) return null;
|
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
|
||||||
return nodes;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadGreatCanonData(): Promise<ServiceNode[] | null> {
|
private async loadGreatCanonData(): Promise<ServiceNode[] | null> {
|
||||||
@@ -617,9 +577,8 @@ export class ServiceView {
|
|||||||
|
|
||||||
const url = `/data/shared/services/canons/great-canon/part${part}.json`;
|
const url = `/data/shared/services/canons/great-canon/part${part}.json`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url);
|
const canonData = await DataCache.fetchWithFallback<GreatCanonPart>(url);
|
||||||
if (!resp.ok) return null;
|
if (!canonData) return null;
|
||||||
const canonData: GreatCanonPart = await resp.json();
|
|
||||||
const ODE_SLAV = ['', 'а҃', 'в҃', 'г҃', 'д҃', 'є҃', 'ѕ҃', 'з҃', 'и҃', 'ѳ҃'];
|
const ODE_SLAV = ['', 'а҃', 'в҃', 'г҃', 'д҃', 'є҃', 'ѕ҃', 'з҃', 'и҃', 'ѳ҃'];
|
||||||
const nodes: ServiceNode[] = [];
|
const nodes: ServiceNode[] = [];
|
||||||
|
|
||||||
@@ -872,12 +831,11 @@ export class ServiceView {
|
|||||||
const dd = String(this.currentDate.getDay()).padStart(2, '0');
|
const dd = String(this.currentDate.getDay()).padStart(2, '0');
|
||||||
const dateKey = `${mm}-${dd}`;
|
const dateKey = `${mm}-${dd}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/data/${this.serviceLang}/menaion-bundle.json`);
|
const bundle = await DataCache.fetchWithFallback<Record<string, unknown>>(`/data/${this.serviceLang}/menaion-bundle.json`);
|
||||||
if (resp.ok) {
|
if (bundle) {
|
||||||
const bundle = await resp.json();
|
|
||||||
const entries = bundle[dateKey];
|
const entries = bundle[dateKey];
|
||||||
if (entries) {
|
if (entries) {
|
||||||
for (const e of entries) {
|
for (const e of entries as Record<string, unknown>[]) {
|
||||||
if (e.id) ids.push(String(e.id));
|
if (e.id) ids.push(String(e.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -885,9 +843,8 @@ export class ServiceView {
|
|||||||
} catch {}
|
} catch {}
|
||||||
for (const cid of ids) {
|
for (const cid of ids) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/data/shared/commemorations/${cid}.json`);
|
const data = await DataCache.fetchWithFallback<Record<string, unknown>>(`/data/shared/commemorations/${cid}.json`);
|
||||||
if (!resp.ok) continue;
|
if (!data) continue;
|
||||||
const data = await resp.json();
|
|
||||||
const scripts = data.SCRIPTURE;
|
const scripts = data.SCRIPTURE;
|
||||||
if (!Array.isArray(scripts)) continue;
|
if (!Array.isArray(scripts)) continue;
|
||||||
let epistlePassage = '';
|
let epistlePassage = '';
|
||||||
@@ -1000,11 +957,9 @@ export class ServiceView {
|
|||||||
const tryFetch = async (lang: string): Promise<string | null> => {
|
const tryFetch = async (lang: string): Promise<string | null> => {
|
||||||
const url = `/data/${lang}/bible/${version}/${book}.text`;
|
const url = `/data/${lang}/bible/${version}/${book}.text`;
|
||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const resp = await fetch(url);
|
const text = await DataCache.fetchWithFallback<string>(url, 'text');
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
if (!text) { this.notFound.add(url); return null; }
|
||||||
const ct = resp.headers.get('content-type');
|
return text;
|
||||||
if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
return resp.text();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = await tryFetch(this.serviceLang);
|
let text = await tryFetch(this.serviceLang);
|
||||||
@@ -1103,15 +1058,12 @@ export class ServiceView {
|
|||||||
const cached = this.livesCache.get(cacheKey);
|
const cached = this.livesCache.get(cacheKey);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const bundleName = /^(0[1-9]|1[0-2])\d+/.test(id) ? id.substring(0, 2) : 'misc';
|
const bundleName = /^(0[1-9]|1[0-2])\d+/.test(id) ? id.substring(0, 2) : `misc/${id.charAt(0)}`;
|
||||||
const tryFetch = async (lang: string): Promise<Record<string, Record<string, Record<string, unknown>>> | null> => {
|
const tryFetch = async (lang: string): Promise<Record<string, Record<string, Record<string, unknown>>> | null> => {
|
||||||
const url = `/data/${lang}/lives/${bundleName}.json`;
|
const url = `/data/${lang}/lives/${bundleName}.json`;
|
||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const resp = await fetch(url);
|
const bundle = await DataCache.fetchWithFallback<Record<string, Record<string, unknown>>>(url);
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
if (!bundle) { this.notFound.add(url); return null; }
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
const bundle = await resp.json() as Record<string, Record<string, unknown>>;
|
|
||||||
const entry = bundle[id] as { services?: Record<string, Record<string, Record<string, unknown>>> } | undefined;
|
const entry = bundle[id] as { services?: Record<string, Record<string, Record<string, unknown>>> } | undefined;
|
||||||
return entry?.services || null;
|
return entry?.services || null;
|
||||||
};
|
};
|
||||||
@@ -1129,15 +1081,12 @@ export class ServiceView {
|
|||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const tryFetch = async (lang: string): Promise<string | null> => {
|
const tryFetch = async (lang: string): Promise<string | null> => {
|
||||||
const url = `/data/${lang}/services/Command/${name}.json`;
|
const url = `/data/${lang}/services/commands/${name}.json`;
|
||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const resp = await fetch(url);
|
const json = await DataCache.fetchWithFallback<Record<string, unknown>[]>(url);
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
if (!json) { this.notFound.add(url); return null; }
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
const json = await resp.json() as Record<string, unknown>[];
|
|
||||||
return json
|
return json
|
||||||
.filter(n => n.type === 'TEXT')
|
.filter(n => n.type === 'TEXT' || n.type === 'text')
|
||||||
.map(n => String(n.value ?? ''))
|
.map(n => String(n.value ?? ''))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ') || null;
|
.join(' ') || null;
|
||||||
@@ -1157,11 +1106,8 @@ export class ServiceView {
|
|||||||
const tryFetch = async (lang: string): Promise<{ cmd: string; value: string }[] | null> => {
|
const tryFetch = async (lang: string): Promise<{ cmd: string; value: string }[] | null> => {
|
||||||
const url = `/data/${lang}/commands/Times.json`;
|
const url = `/data/${lang}/commands/Times.json`;
|
||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const resp = await fetch(url);
|
const json = await DataCache.fetchWithFallback<{ DATA?: { TIMES?: { '@_Value': string; '@_Cmd': string }[] } }>(url);
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
if (!json) { this.notFound.add(url); return null; }
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
const json = await resp.json() as { DATA: { TIMES: { '@_Value': string; '@_Cmd': string }[] } };
|
|
||||||
const arr = json?.DATA?.TIMES;
|
const arr = json?.DATA?.TIMES;
|
||||||
if (!Array.isArray(arr)) return null;
|
if (!Array.isArray(arr)) return null;
|
||||||
return arr.map(r => ({ value: r['@_Value'], cmd: r['@_Cmd'] }));
|
return arr.map(r => ({ value: r['@_Value'], cmd: r['@_Cmd'] }));
|
||||||
@@ -1227,22 +1173,13 @@ export class ServiceView {
|
|||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const cached = this.jsonCache.get(url);
|
const cached = this.jsonCache.get(url);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
try {
|
const json = await DataCache.fetchWithFallback<Record<string, unknown>[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
|
||||||
const ct = resp.headers.get('content-type');
|
|
||||||
if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
const json = JSON.parse(await resp.text());
|
|
||||||
if (Array.isArray(json)) {
|
if (Array.isArray(json)) {
|
||||||
this.jsonCache.set(url, json);
|
this.jsonCache.set(url, json);
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
this.notFound.add(url);
|
this.notFound.add(url);
|
||||||
return null;
|
return null;
|
||||||
} catch {
|
|
||||||
this.notFound.add(url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNodes = (json: Record<string, unknown>[], header: boolean): string | null => {
|
const filterNodes = (json: Record<string, unknown>[], header: boolean): string | null => {
|
||||||
@@ -1254,7 +1191,7 @@ export class ServiceView {
|
|||||||
.join(' ') || null;
|
.join(' ') || null;
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
.filter(n => n.type === 'TEXT')
|
.filter(n => n.type === 'TEXT' || n.type === 'text')
|
||||||
.map(n => String(n.value ?? '').replace(/\n/g, ' '))
|
.map(n => String(n.value ?? '').replace(/\n/g, ' '))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('<br>') || null;
|
.join('<br>') || null;
|
||||||
@@ -1297,22 +1234,13 @@ export class ServiceView {
|
|||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const cached = this.nodeCache.get(url);
|
const cached = this.nodeCache.get(url);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
try {
|
const nodes = await DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
|
||||||
const ct = resp.headers.get('content-type') || '';
|
|
||||||
if (ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
const nodes = await resp.json();
|
|
||||||
if (Array.isArray(nodes)) {
|
if (Array.isArray(nodes)) {
|
||||||
this.nodeCache.set(url, nodes);
|
this.nodeCache.set(url, nodes);
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
this.notFound.add(url);
|
this.notFound.add(url);
|
||||||
return null;
|
return null;
|
||||||
} catch {
|
|
||||||
this.notFound.add(url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1373,15 +1301,10 @@ export class ServiceView {
|
|||||||
if (this.notFound.has(url)) return null;
|
if (this.notFound.has(url)) return null;
|
||||||
const cached = this.nodeCache.get(url);
|
const cached = this.nodeCache.get(url);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
try {
|
const nodes = await DataCache.fetchWithFallback<ServiceNode[]>(url);
|
||||||
const resp = await fetch(url);
|
if (!nodes) { this.notFound.add(url); return null; }
|
||||||
if (!resp.ok) { this.notFound.add(url); return null; }
|
|
||||||
const ct = resp.headers.get('content-type') || '';
|
|
||||||
if (ct.includes('text/html')) { this.notFound.add(url); return null; }
|
|
||||||
const nodes = await resp.json();
|
|
||||||
this.nodeCache.set(url, nodes);
|
this.nodeCache.set(url, nodes);
|
||||||
return nodes;
|
return nodes;
|
||||||
} catch { this.notFound.add(url); return null; }
|
|
||||||
};
|
};
|
||||||
let url = resolvePath(this.serviceLang);
|
let url = resolvePath(this.serviceLang);
|
||||||
if (url) {
|
if (url) {
|
||||||
|
|||||||
@@ -326,8 +326,8 @@ export class SettingsView {
|
|||||||
// Load default Bible versions dynamically
|
// Load default Bible versions dynamically
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/bible/versions.json');
|
const versions = await DataCache.fetchWithFallback<any[]>('/data/bible/versions.json');
|
||||||
const versions = await resp.json();
|
if (!versions || !Array.isArray(versions)) return;
|
||||||
const userLang = this.settings.language;
|
const userLang = this.settings.language;
|
||||||
|
|
||||||
// Sort by language priority
|
// Sort by language priority
|
||||||
@@ -479,8 +479,8 @@ export class SettingsView {
|
|||||||
// Load translations on first expand
|
// Load translations on first expand
|
||||||
if (container.innerHTML === '') {
|
if (container.innerHTML === '') {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/bible/versions.json');
|
const versions = await DataCache.fetchWithFallback<any[]>('/data/bible/versions.json');
|
||||||
const versions = await resp.json();
|
if (!versions || !Array.isArray(versions)) throw new Error('unavailable');
|
||||||
const userLang = this.settings.language;
|
const userLang = this.settings.language;
|
||||||
versions.sort((a: any, b: any) => {
|
versions.sort((a: any, b: any) => {
|
||||||
const pa = versionPriority(a.language, userLang);
|
const pa = versionPriority(a.language, userLang);
|
||||||
@@ -637,8 +637,8 @@ export class SettingsView {
|
|||||||
// Count cached Bible translations
|
// Count cached Bible translations
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/data/bible/versions.json');
|
const versions = await DataCache.fetchWithFallback<any[]>('/data/bible/versions.json');
|
||||||
const versions = await resp.json();
|
if (!versions || !Array.isArray(versions)) return;
|
||||||
const total = versions.length;
|
const total = versions.length;
|
||||||
let cached = 0;
|
let cached = 0;
|
||||||
for (const v of versions) {
|
for (const v of versions) {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":", after each"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"Bow"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Forty times)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Sticheron Melody: Tone 6)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"Prostration"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Stichos 1)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Stichos 2)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 1)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 2)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 3)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 4)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 5)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 6)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 7)"}]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"type":"TEXT","value":"(Tone 8)"}]
|
|
||||||
Reference in new issue
Block a user