feat: unified offline preload — Calendar & Services + Bible, 3 checkboxes → 1
This commit is contained in:
1 parent
e4ddee924b
commit
fdbfcd4911
4 files changed
+230
-39
No files matched your search
+3
-3
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user