feat: unified offline preload — Calendar & Services + Bible, 3 checkboxes → 1

This commit is contained in:
catarrh committed 2026-08-04 02:16:15 +03:00
1 parent e4ddee924b
commit fdbfcd4911
4 files changed
+230 -39

No files matched your search

+3 -3
View File
@@ -1065,7 +1065,7 @@ const translations: Record<LanguageCode, Translations> = {
offlineLangs: 'Languages to cache',
offlineDataTypes: 'Data to cache',
offlineLives: 'Lives',
offlineCalendar: 'Calendar',
offlineCalendar: 'Calendar & Services',
offlineMenaion: 'Menaion',
offlineBible: 'Bible',
offlinePreload: 'Preload',
@@ -1624,7 +1624,7 @@ const translations: Record<LanguageCode, Translations> = {
offlineLangs: 'Языки для кэширования',
offlineDataTypes: 'Типы данных',
offlineLives: 'Жития',
offlineCalendar: 'Календарь',
offlineCalendar: 'Календарь и службы',
offlineMenaion: 'Минея',
offlineBible: 'Библия',
offlinePreload: 'Загрузить',
@@ -2183,7 +2183,7 @@ const translations: Record<LanguageCode, Translations> = {
offlineLangs: 'Языки для кеширования',
offlineDataTypes: 'Типы данныхъ',
offlineLives: 'Житїя',
offlineCalendar: 'Календарь',
offlineCalendar: 'Календарь и службы',
offlineMenaion: 'Минїа',
offlineBible: 'Би́блїа',
offlinePreload: 'Загрузити',
+225 -26
View File
@@ -8,7 +8,7 @@
* Usage:
* const progress = await OfflineManager.preload({
* languages: ['ru', 'cu'],
* types: ['lives', 'calendar'],
* types: ['calendar', 'bible'],
* });
* const stats = await OfflineManager.getStats();
* await OfflineManager.clearCache();
@@ -18,7 +18,7 @@ import { DataCache } from './data-cache';
export interface PreloadOptions {
languages: string[];
types: ('lives' | 'bible' | 'calendar' | 'menaion')[];
types: ('calendar' | 'bible')[];
bibleTranslations?: string[];
}
@@ -46,32 +46,231 @@ const BIBLE_SIZES: Record<string, string> = {
'zh/Hans/bible/cuv': '3.7 MB', 'zh/Hant/bible/cuv': '3.7 MB',
};
const SERVICE_TEMPLATES = [
'Template', 'Vespers', 'Matins', 'DivineLiturgy', 'GreatCompline', 'UsualBeginning',
'TrisagionBlock', 'Prime', 'ThirdHour', 'SixthHour', 'NinthHour', 'RoyalHours',
'PaschalHours', 'SaturdayMidnight', 'SaturdayVespersLiturgy', 'SaturdayHours',
'BurialVespers', 'MariasStanding', 'Pascha', 'Lamentations', 'PassionGospels', 'RoyalHoursFriday',
...Array.from({ length: 20 }, (_, i) => `Kathisma${i + 1}`),
'BrightMonday', 'BrightTuesday', 'BrightWednesday', 'BrightThursday', 'BrightFriday', 'BrightSaturday',
'FirstWeekMonday', 'FirstWeekTuesday', 'FirstWeekWednesday', 'FirstWeekThursday', 'FirstWeekFriday', 'FirstWeekSaturday',
'Antipascha', 'Myrrhbearers', 'Paralytic', 'Samaritan', 'BlindMan', 'Prepolovenie',
'Apodosis', 'Ascension', 'PentecostSaturday', 'HolyFathers', 'HolySpirit', 'Pentecost', 'AllSaints', 'RussianSaints',
'NativityTheotokos', 'Vvedenie', 'Exaltation', 'NativityHours', 'Nativity',
'TheophanyHours', 'Theophany', 'Sretenie', 'Annunciation', 'PalmSunday', 'Transfiguration', 'Dormition',
'Circumcision', 'ForerunnerBirth', 'PeterPaul', 'ForerunnerBeheading', 'Pokrov', 'MichaelSynaxis',
'Vladimir', 'Kazan', 'JohnTheologianSep', 'Sergius', 'Ambrose', 'Demetrius', 'Nicholas',
'NicholasTranslation', 'FindingHead1st', 'FindingHead3rd', 'FortyMartyrs', 'JohnTheologianMay',
'Panteleimon', 'Elijah', 'ProcessionCross', 'SixCouncilsFathers', 'SeventhCouncilFathers',
'ForefathersSunday', 'HolyFathersNativity', 'SundayAfterNativity', 'MenaionDay', 'Triodion',
];
const FEAST_NAMES = [
'allsaints', 'ambrose', 'annunciation', 'antipascha', 'apodosis', 'ascension', 'blindman',
'bright-friday', 'bright-monday', 'bright-saturday', 'bright-thursday', 'bright-tuesday', 'bright-wednesday',
'burial-vespers', 'circumcision', 'demetrius', 'dormition', 'elijah', 'exaltation',
'finding-head-1st', 'finding-head-3rd',
'first-week-friday', 'first-week-monday', 'first-week-saturday', 'first-week-thursday', 'first-week-tuesday', 'first-week-wednesday',
'forefathers-sunday', 'forerunner-beheading', 'forerunner-birth', 'forty-martyrs',
'great-monday', 'great-thursday', 'great-tuesday', 'great-wednesday',
'holy-fathers-nativity', 'holyfathers', 'holyspirit',
'johntheologian-may', 'johntheologian-sep', 'kazan', 'lamentations',
'marias-standing', 'michael-synaxis', 'myrrhbearers',
'nativity-hours', 'nativity-theotokos', 'nativity', 'nicholas-translation', 'nicholas',
'palm-sunday', 'panteleimon', 'paralytic', 'pascha', 'passion-gospels', 'pentecost',
'pentecostsaturday', 'peter-paul', 'pokrov', 'prepolovenie', 'procession-cross',
'royal-hours-friday', 'russiansaints', 'samaritan',
'saturday-hours', 'saturday-midnight', 'saturday-vespers-liturgy', 'sergius',
'seventh-council-fathers', 'six-councils-fathers', 'sretenie', 'sunday-after-nativity',
'theophany-hours', 'theophany', 'transfiguration', 'vladimir', 'vvedenie',
];
const PRAYER_COLLECTIONS: Record<string, string[]> = {
akathists: [
'akathist-jesus', 'akathist-nicholas', 'akathist-theotokos', 'alexander-svirsky',
'angels-canon', 'cross-canon', 'cyprian-justina', 'forerunner-canon', 'gabriel-canon',
'guardian-angel', 'jesus-compunction', 'jesus-penitential', 'john-kronstadt',
'mary-egypt', 'michael-canon', 'murom-wonderworkers', 'nativity-canon',
'nicholas-canon-akathist', 'panteleimon', 'pascha-canon', 'pokrov-canon',
'saint-anne', 'seraphim', 'sergius', 'skoroposlushnitsa', 'spiridon',
'theotokos-moleben', 'theotokos-nativity', 'theotokos-thanksgiving',
'trinity-canon', 'troeruchitsa', 'tryphon', 'utoli-pechali',
],
horologion: [
'interhour-1', 'interhour-3', 'interhour-6', 'interhour-9', 'meal-blessing',
'midnight-daily', 'midnight-saturday', 'midnight-sunday', 'panagia',
'small-compline', 'typica',
],
'prayer-rule': [
'beginning-ending', 'communion', 'diptychs', 'evening', 'litia-departed',
'morning', 'rule-impurity', 'thanksgiving', 'three-canons', 'twelve-psalms',
],
sbornik: [
'biblical-songs-daily', 'biblical-songs-feasts', 'biblical-songs-lent',
'common-trop-kont', 'exapostilaria-week', 'feast-trop-kont', 'katavasia',
'lamps-weekday', 'lent-trop-kont', 'pentecost-trop-kont', 'sunday-trop-kont',
'theotokion-8tones', 'theotokion-dismissal', 'theotokion-sunday',
'trinity-troparia', 'weekday-trop-kont',
],
irmologion: [
'chosen-psalms', 'feast-refrains-ode9', 'gospodi-vozzvah',
'irmos-1', 'irmos-2', 'irmos-3', 'irmos-4', 'irmos-5', 'irmos-6', 'irmos-7', 'irmos-8',
'irmos-prefeast-nativity', 'irmos-prefeast-theophany',
'lent-canon-rules', 'liturgy-chants', 'paschal-canon',
'saturday-troparia', 'stepenna', 'sunday-feast-verses', 'sunday-prokimena',
'sunday-troparia', 'theotokia-daily', 'theotokia-sunday', 'trinity-songs',
],
};
const PARIMII_DIRS = [
'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec',
'cheese-week', 'common-saints', 'holy-week', 'palm-week', 'pentecostarion',
'lent-week-1', 'lent-week-2', 'lent-week-3', 'lent-week-4', 'lent-week-5',
];
function daysInMonth(m: number): number {
return [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1];
}
/** Generate calendar & services data files for a given language. */
function calendarDataFiles(lang: string): string[] {
const files: string[] = [];
// Lives: month files
for (let m = 1; m <= 12; m++) {
files.push(`/data/${lang}/lives/${String(m).padStart(2, '0')}.json`);
}
// Lives: misc files
for (let d = 0; d <= 9; d++) {
files.push(`/data/${lang}/lives/misc/${d}.json`);
}
// Shared lives index
files.push('/data/shared/lives-index.json');
// Calendar shared
files.push('/data/shared/fasting.json');
files.push('/data/shared/calendar/triodion.json');
files.push('/data/shared/calendar/pentecostarion.json');
// Menaion bundle
files.push(`/data/${lang}/menaion-bundle.json`);
// Service templates
for (const t of SERVICE_TEMPLATES) {
files.push(`/data/shared/services/templates/${t}.json`);
}
// Feast data
for (const f of FEAST_NAMES) {
files.push(`/data/shared/services/${f}/full.json`);
}
// Paraclete day files (8 tones × 6 days)
for (let tone = 1; tone <= 8; tone++) {
for (const day of ['mon', 'tue', 'wed', 'thu', 'fri', 'sat']) {
files.push(`/data/shared/services/paraclete/tone${tone}/${day}.json`);
}
}
// Service canons
for (let tone = 0; tone <= 7; tone++) {
files.push(`/data/shared/services/canons/tone${tone}/sunday.json`);
}
for (let tone = 1; tone <= 8; tone++) {
for (const day of ['mon', 'tue', 'wed', 'thu', 'fri', 'sat']) {
files.push(`/data/shared/services/canons/paraclete/tone${tone}/${day}.json`);
}
}
files.push('/data/shared/services/canons/great-canon/part1.json');
files.push('/data/shared/services/canons/great-canon/part2.json');
files.push('/data/shared/services/canons/great-canon/part3.json');
files.push('/data/shared/services/canons/great-canon/part4.json');
// Menaion daily index + sections
files.push('/data/shared/menaion-daily/index.json');
for (let m = 1; m <= 12; m++) {
for (let d = 1; d <= daysInMonth(m); d++) {
const dateKey = `${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
for (let n = 1; n <= 4; n++) {
files.push(`/data/shared/menaion-daily/${dateKey}/${n}.json`);
}
}
}
// Triodion
for (let i = 1; i <= 37; i++) {
files.push(`/data/shared/triodion/${String(i).padStart(2, '0')}.json`);
}
// Pentecostarion
for (let i = 1; i <= 315; i++) {
files.push(`/data/shared/pentecostarion/${String(i).padStart(2, '0')}.json`);
}
// Menaion common + add
for (let i = 1; i <= 42; i++) files.push(`/data/shared/menaion-common/${String(i).padStart(2, '0')}.json`);
for (let i = 1; i <= 51; i++) files.push(`/data/shared/menaion-add/${String(i).padStart(2, '0')}.json`);
// Prayer collections
for (const [collection, items] of Object.entries(PRAYER_COLLECTIONS)) {
for (const item of items) {
files.push(`/data/shared/${collection}/${item}/full.json`);
}
}
// Horologion additions
const horologionAddItems = [
'exapostilaria', 'katavasia', 'lamps', 'songs-daily', 'songs-feasts', 'songs-lent',
'theotokia-8tones', 'theotokia-dismissal', 'theotokia-sunday', 'trinity-8tones',
'trop-common', 'trop-daily', 'trop-lent', 'trop-pentecost', 'trop-sunday',
];
for (const item of horologionAddItems) {
files.push(`/data/shared/horologionadd/${item}/full.json`);
}
for (const m of ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']) {
files.push(`/data/shared/horologionadd/trop-feasts-${m}/full.json`);
}
// Parimii
for (const dir of PARIMII_DIRS) {
files.push(`/data/shared/parimii/${dir}/full.json`);
}
// Commemorations
for (const id of ['134', '35', '373', '543', 'F0', 'F6']) {
files.push(`/data/shared/commemorations/${id}.json`);
}
// Language-specific commands
files.push(`/data/${lang}/commands/Times.json`);
files.push(`/data/${lang}/commands/LanguagePacks.json`);
files.push(`/data/${lang}/commands/Podobni.json`);
files.push(`/data/${lang}/commands/RuleBasedNumbers.json`);
// Language-specific octoecheos (8 tones × 7 days)
for (let tone = 0; tone <= 7; tone++) {
for (const day of ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']) {
files.push(`/data/${lang}/services/octoecheos/tone${tone}/${day}.json`);
}
}
// Language-specific service commands
for (let tone = 1; tone <= 8; tone++) {
files.push(`/data/${lang}/services/commands/Tone${tone}.json`);
}
files.push(`/data/${lang}/services/commands/AfterEach.json`);
files.push(`/data/${lang}/services/commands/Bow.json`);
files.push(`/data/${lang}/services/commands/Prostration.json`);
files.push(`/data/${lang}/services/commands/S1.json`);
files.push(`/data/${lang}/services/commands/S2.json`);
return files;
}
/** Data files to preload per language, grouped by type. */
const DATA_FILES: Record<string, (lang: string) => string[]> = {
lives: (lang) => {
const files: string[] = [];
// Month-prefixed files
for (let m = 1; m <= 12; m++) {
files.push(`/data/${lang}/lives/${String(m).padStart(2, '0')}.json`);
}
// Misc split files (by first digit)
for (let d = 0; d <= 9; d++) {
files.push(`/data/${lang}/lives/misc/${d}.json`);
}
return files;
},
calendar: () => [
'/data/shared/fasting.json',
'/data/shared/calendar/triodion.json',
'/data/shared/calendar/pentecostarion.json',
'/data/shared/lives-index.json',
],
menaion: (lang) => [
`/data/${lang}/menaion-bundle.json`,
],
bible: (_lang) => [
'/data/bible/versions.json',
],
calendar: (lang) => calendarDataFiles(lang),
bible: () => ['/data/bible/versions.json'],
};
export class OfflineManager {
+1 -1
View File
@@ -957,7 +957,7 @@ commPanel.addEventListener('click', (e) => {
<span class="text-navy">${solar?.sunset ?? '—'}</span>
<span class="mx-2 text-gold/40">|</span>
<span class="font-bold text-navy">${this.t.calendar.moonPhase}:</span>
<span class="text-navy"><span title="${moonName}">${moon.emoji}</span> ${moon.trend === 'waxing' ? '↗' : '↘'} ${this.t.calendar.moonDay} ${moon.moonDay}, ${moon.illumination}%</span>
<span class="text-navy"><span title="${moonName}">${moon.emoji}</span> ${moon.trend === 'waxing' ? '↗' : '↘'}</span>
`;
} else {
astroPanel.innerHTML = `<span class="text-navy-light italic">${this.t.calendar.noLocation}</span>`;
+1 -9
View File
@@ -301,15 +301,9 @@ export class SettingsView {
<div class="mb-3">
<label class="block text-sm font-bold text-navy mb-2">${t.settings.offlineDataTypes}</label>
<div id="offline-types" class="flex flex-wrap gap-3">
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" value="lives" checked class="offline-type accent-gold"> <span class="text-sm">${t.settings.offlineLives}</span>
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" value="calendar" checked class="offline-type accent-gold"> <span class="text-sm">${t.settings.offlineCalendar}</span>
</label>
<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>
</label>
<div id="bible-row" class="flex items-center gap-1 cursor-pointer">
<span class="text-sm">${t.settings.offlineBible} <span id="bible-count" class="text-xs text-navy-light">(0/0)</span></span>
<span id="bible-chevron" class="text-xs text-navy-light hover:text-navy select-none">▶</span>
@@ -505,7 +499,7 @@ export class SettingsView {
const selectedLangs = Array.from(document.querySelectorAll('.offline-lang:checked'))
.map(el => (el as HTMLInputElement).value);
const selectedTypes = Array.from(document.querySelectorAll('.offline-type:checked'))
.map(el => (el as HTMLInputElement).value) as ('lives' | 'bible' | 'calendar' | 'menaion')[];
.map(el => (el as HTMLInputElement).value) as ('calendar' | 'bible')[];
if (selectedLangs.length === 0) {
const progressDiv = document.getElementById('offline-progress');
@@ -584,9 +578,7 @@ export class SettingsView {
};
// 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 () => {