diff --git a/src/core/i18n.ts b/src/core/i18n.ts index 69c6b66..9a61b34 100644 --- a/src/core/i18n.ts +++ b/src/core/i18n.ts @@ -1065,7 +1065,7 @@ const translations: Record = { 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 = { offlineLangs: 'Языки для кэширования', offlineDataTypes: 'Типы данных', offlineLives: 'Жития', - offlineCalendar: 'Календарь', + offlineCalendar: 'Календарь и службы', offlineMenaion: 'Минея', offlineBible: 'Библия', offlinePreload: 'Загрузить', @@ -2183,7 +2183,7 @@ const translations: Record = { offlineLangs: 'Языки для кеширования', offlineDataTypes: 'Типы данныхъ', offlineLives: 'Житїя', - offlineCalendar: 'Календарь', + offlineCalendar: 'Календарь и службы', offlineMenaion: 'Минїа', offlineBible: 'Би́блїа', offlinePreload: 'Загрузити', diff --git a/src/core/offline-manager.ts b/src/core/offline-manager.ts index 8d3af6f..f323572 100644 --- a/src/core/offline-manager.ts +++ b/src/core/offline-manager.ts @@ -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 = { '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 = { + 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[]> = { - 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 { diff --git a/src/ui/calendar-view.ts b/src/ui/calendar-view.ts index 3014e36..4d495f1 100644 --- a/src/ui/calendar-view.ts +++ b/src/ui/calendar-view.ts @@ -957,7 +957,7 @@ commPanel.addEventListener('click', (e) => { ${solar?.sunset ?? '—'} | ${this.t.calendar.moonPhase}: - ${moon.emoji} ${moon.trend === 'waxing' ? '↗' : '↘'} ${this.t.calendar.moonDay} ${moon.moonDay}, ${moon.illumination}% + ${moon.emoji} ${moon.trend === 'waxing' ? '↗' : '↘'} `; } else { astroPanel.innerHTML = `${this.t.calendar.noLocation}`; diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index c9e4d90..2aef76d 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -301,15 +301,9 @@ export class SettingsView {
- -
${t.settings.offlineBible} (0/0) ▶ @@ -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 () => {