Matins completed

This commit is contained in:
catarrh committed 2026-08-01 02:43:19 +03:00
1 parent ff5b8dc649
commit 79ac617aa8
4 files changed
+1933 -16

No files matched your search

+1321
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -417,6 +417,15 @@ export async function assembleService(
break;
}
case 'HEADER': {
flushGroup();
const value = str(n.value);
if (value) {
html += `<h3 class="mt-4 mb-1 font-bold text-center">${value}</h3>\n`;
}
break;
}
case 'GETID': {
const whoRaw = str(n.who);
const who = whoRaw !== undefined ? localizeWho(whoRaw, ctx) : '';
+40 -2
View File
@@ -4,12 +4,14 @@
import { CalendarView } from './calendar-view';
import { BibleView } from './bible-view';
import { PrayerView } from './prayer-view';
import { MenaionView } from './menaion-view';
import { SettingsView } from './settings-view';
import { JDate } from '../core/jdate';
import { loadSettings, type Settings } from './settings-view';
import { getTranslations, type LanguageCode } from '../core/i18n';
type View = 'calendar' | 'bible' | 'settings';
type View = 'calendar' | 'bible' | 'prayer' | 'akathists' | 'parimii' | 'horologion' | 'sbornik' | 'paraclete' | 'irmologion' | 'menaion' | 'triodion' | 'settings';
export class App {
private container: HTMLElement;
@@ -72,9 +74,18 @@ export class App {
<div class="app-container min-h-screen flex flex-col">
<header class="bg-navy text-parchment p-3 flex items-center justify-between shadow-md">
<h1 class="text-sm font-bold tracking-wide">${t.appName}</h1>
<nav class="flex gap-4 text-sm">
<nav class="flex flex-wrap gap-4 text-sm">
<a href="#calendar" class="nav-link hover:text-gold transition-colors" data-view="calendar">${t.nav.calendar}</a>
<a href="#bible" class="nav-link hover:text-gold transition-colors" data-view="bible">${t.nav.bible}</a>
<a href="#prayer" class="nav-link hover:text-gold transition-colors" data-view="prayer">${t.nav.prayer}</a>
<a href="#akathists" class="nav-link hover:text-gold transition-colors" data-view="akathists">${t.nav.akathists}</a>
<a href="#parimii" class="nav-link hover:text-gold transition-colors" data-view="parimii">${t.nav.parimii}</a>
<a href="#horologion" class="nav-link hover:text-gold transition-colors" data-view="horologion">${t.nav.horologion}</a>
<a href="#sbornik" class="nav-link hover:text-gold transition-colors" data-view="sbornik">${t.nav.sbornik}</a>
<a href="#paraclete" class="nav-link hover:text-gold transition-colors" data-view="paraclete">${t.nav.paraclete}</a>
<a href="#irmologion" class="nav-link hover:text-gold transition-colors" data-view="irmologion">${t.nav.irmologion}</a>
<a href="#menaion" class="nav-link hover:text-gold transition-colors" data-view="menaion">${t.nav.menaion}</a>
<a href="#triodion" class="nav-link hover:text-gold transition-colors" data-view="triodion">${t.nav.triodion}</a>
<a href="#settings" class="nav-link hover:text-gold transition-colors" data-view="settings">${t.nav.settings}</a>
</nav>
</header>
@@ -127,6 +138,33 @@ export class App {
new BibleView(viewContainer, lang, bibleVersion, bibleBook, biblePassage).render();
break;
}
case 'prayer':
new PrayerView(viewContainer, lang, 'prayer-rule').render();
break;
case 'akathists':
new PrayerView(viewContainer, lang, 'akathists').render();
break;
case 'parimii':
new PrayerView(viewContainer, lang, 'parimii').render();
break;
case 'horologion':
new PrayerView(viewContainer, lang, 'horologion').render();
break;
case 'sbornik':
new PrayerView(viewContainer, lang, 'sbornik').render();
break;
case 'paraclete':
new PrayerView(viewContainer, lang, 'paraclete').render();
break;
case 'irmologion':
new PrayerView(viewContainer, lang, 'irmologion').render();
break;
case 'menaion':
new MenaionView(viewContainer, lang).render();
break;
case 'triodion':
new PrayerView(viewContainer, lang, 'triodion').render();
break;
case 'settings':
new SettingsView(viewContainer, () => {
this.render();
+563 -14
View File
@@ -19,6 +19,10 @@ interface ServiceDef {
template: string;
description: string;
titleNode: ServiceNode;
divider?: boolean;
menaionDayIndex?: number;
menaionDateKey?: string;
triodionIndex?: number;
}
function title(value: string, header?: string, source?: string, comment?: string): ServiceNode {
@@ -41,6 +45,7 @@ export class ServiceView {
private commandCache = new Map<string, string>();
private timesCache = new Map<string, { cmd: string; value: string }[]>();
private activeService: string | null = null;
private menaionDayCount = new Map<string, number>();
constructor(container: HTMLElement, date: JDate, language: LanguageCode = 'en') {
this.container = container;
@@ -63,6 +68,17 @@ export class ServiceView {
const computed = computeDay(this.currentDate);
const { dayInfo } = computed;
// Load the daily Menaion index (date -> section count) for the Служба дня tabs
try {
const resp = await fetch('/data/shared/menaion-daily/index.json');
if (resp.ok) {
const index = await resp.json() as Record<string, number>;
for (const [dk, n] of Object.entries(index)) {
this.menaionDayCount.set(dk, n);
}
}
} catch { /* keep empty cache */ }
const availableServices = this.getAvailableServices(dayInfo);
const fontClass = this.getFontClass();
@@ -72,6 +88,7 @@ export class ServiceView {
<h3 class="font-bold text-navy mb-3">${this.t.services.title}</h3>
<div class="flex flex-wrap gap-1 mb-6 border-b border-gold/20">
${availableServices.map(s => `
${s.divider ? '<span class="self-center mx-1 text-gold/40 select-none" aria-hidden="true">·</span>' : ''}
<button
class="service-tab px-4 py-2 text-sm border-b-2 transition-colors whitespace-nowrap
${this.activeService === s.id
@@ -124,51 +141,100 @@ export class ServiceView {
}
private getAvailableServices(dayInfo: import('../core/types').DayInfo): ServiceDef[] {
const services: ServiceDef[] = [];
const special: ServiceDef[] = [];
const daily: ServiceDef[] = [];
services.push({
// Triodion movable days (by nday): Pre-Lent Sundays + Lent Sundays
// (the ones not already covered: week 1, Marias Standing, Palm Sunday, Holy Week)
const triodionDay: Record<number, { idx: number; name: string }> = {
[-70]: { idx: 1, name: this.t.triodion.sections[0] },
[-63]: { idx: 2, name: this.t.triodion.sections[1] },
[-57]: { idx: 3, name: this.t.triodion.sections[2] },
[-56]: { idx: 4, name: this.t.triodion.sections[3] },
[-50]: { idx: 6, name: this.t.triodion.sections[5] },
[-49]: { idx: 7, name: this.t.triodion.sections[6] },
[-42]: { idx: 14, name: this.t.triodion.sections[13] },
[-35]: { idx: 17, name: this.t.triodion.sections[16] },
[-28]: { idx: 20, name: this.t.triodion.sections[19] },
[-21]: { idx: 23, name: this.t.triodion.sections[22] },
[-14]: { idx: 27, name: this.t.triodion.sections[26] },
[-9]: { idx: 26, name: this.t.triodion.sections[25] },
[-8]: { idx: 29, name: this.t.triodion.sections[28] },
};
const triodionHit = triodionDay[dayInfo.nday];
if (triodionHit) {
special.push({
id: `triodion-${triodionHit.idx}`,
name: triodionHit.name,
serviceType: 'TRIODION',
template: 'Triodion',
description: this.t.triodion.title,
titleNode: title('Triodion1', triodionHit.name),
triodionIndex: triodionHit.idx,
});
}
// Daily Menaion: the day's proper service (menaion-daily/{MM-DD}/{NN}.json)
const mmDay = `${String(dayInfo.month).padStart(2, '0')}-${String(dayInfo.day).padStart(2, '0')}`;
const menaionDayCount = this.menaionDayCount.get(mmDay) ?? -1;
if (menaionDayCount === -1) {
// Cache lookup is populated in render(); fall back to 0 (fetched async)
}
for (let i = 1; i <= Math.max(0, menaionDayCount); i++) {
special.push({
id: `menaion-day-${i}`, name: i === 1 ? this.t.services.serviceNames.menaionDay : this.t.services.serviceNames.menaionDay,
serviceType: 'MENAIONDAY',
template: 'MenaionDay',
description: this.t.services.serviceDescriptions.menaionDay,
titleNode: title('MenaionDay1', this.t.services.serviceNames.menaionDay),
menaionDayIndex: i,
menaionDateKey: mmDay,
});
}
daily.push({
id: 'primes', name: this.t.services.serviceNames.primes, serviceType: 'PRIMES',
template: 'Prime',
description: this.t.services.serviceDescriptions.primes,
titleNode: title('Primes1', this.t.services.serviceNames.primes, 'PrimeSource'),
});
services.push({
daily.push({
id: 'third', name: this.t.services.serviceNames.third, serviceType: 'TERCE',
template: 'ThirdHour',
description: this.t.services.serviceDescriptions.third,
titleNode: title('Third1', this.t.services.serviceNames.third),
});
services.push({
daily.push({
id: 'sixth', name: this.t.services.serviceNames.sixth, serviceType: 'SEXTE',
template: 'SixthHour',
description: this.t.services.serviceDescriptions.sixth,
titleNode: title('Sixth1', this.t.services.serviceNames.sixth),
});
services.push({
daily.push({
id: 'ninth', name: this.t.services.serviceNames.ninth, serviceType: 'NONE',
template: 'NinthHour',
description: this.t.services.serviceDescriptions.ninth,
titleNode: title('Ninth1', this.t.services.serviceNames.ninth),
});
services.push({
daily.push({
id: 'vespers', name: this.t.services.serviceNames.vespers, serviceType: 'VESPERS',
template: 'Vespers',
description: this.t.services.serviceDescriptions.vespers,
titleNode: title('Vespers1', this.t.services.serviceNames.vespers, 'VesperSource'),
});
services.push({
daily.push({
id: 'matins', name: this.t.services.serviceNames.matins, serviceType: 'MATINS',
template: 'Matins',
description: this.t.services.serviceDescriptions.matins,
titleNode: title('Matins1', this.t.services.serviceNames.matins, 'MatinsSource'),
});
services.push({
daily.push({
id: 'liturgy', name: this.t.services.serviceNames.liturgy, serviceType: 'LITURGY',
template: 'DivineLiturgy',
description: this.t.services.serviceDescriptions.liturgy,
@@ -176,7 +242,7 @@ export class ServiceView {
});
if (dayInfo.dRank >= 6) {
services.push({
daily.push({
id: 'royalhours', name: this.t.services.serviceNames.royalhours, serviceType: 'ROYALHOURS',
template: 'RoyalHours',
description: this.t.services.serviceDescriptions.royalhours,
@@ -184,9 +250,162 @@ export class ServiceView {
});
}
// Bright Week services (Pascha through Saturday)
if (dayInfo.isBrightWeek) {
const brightDay: Record<number, { id: string; name: string; type: string; desc: string }> = {
[0]: { id: 'pascha', name: this.t.services.serviceNames.pascha, type: 'PASCHA', desc: this.t.services.serviceDescriptions.pascha },
[1]: { id: 'brightmonday', name: this.t.services.serviceNames.brightmonday, type: 'BRIGHTMONDAY', desc: this.t.services.serviceDescriptions.brightmonday },
[2]: { id: 'brighttuesday', name: this.t.services.serviceNames.brighttuesday, type: 'BRIGHTTUESDAY', desc: this.t.services.serviceDescriptions.brighttuesday },
[3]: { id: 'brightwednesday', name: this.t.services.serviceNames.brightwednesday, type: 'BRIGHTWEDNESDAY', desc: this.t.services.serviceDescriptions.brightwednesday },
[4]: { id: 'brightthursday', name: this.t.services.serviceNames.brightthursday, type: 'BRIGHTTHURSDAY', desc: this.t.services.serviceDescriptions.brightthursday },
[5]: { id: 'brightfriday', name: this.t.services.serviceNames.brightfriday, type: 'BRIGHTFRIDAY', desc: this.t.services.serviceDescriptions.brightfriday },
[6]: { id: 'brightsaturday', name: this.t.services.serviceNames.brightsaturday, type: 'BRIGHTSATURDAY', desc: this.t.services.serviceDescriptions.brightsaturday },
};
const bd = brightDay[dayInfo.nday];
if (bd) {
const templateName = bd.id.charAt(0).toUpperCase() + bd.id.slice(1);
special.push({
id: bd.id, name: bd.name, serviceType: bd.type,
template: templateName,
description: bd.desc,
titleNode: title(`${templateName}1`, bd.name),
});
}
}
// Pentecostarion services (Antipascha through All Saints)
if (dayInfo.pentecostarionFile !== null && dayInfo.nday > 6) {
const pentDay: Record<number, { id: string; name: string; type: string; desc: string }> = {
[7]: { id: 'antipascha', name: this.t.services.serviceNames.antipascha, type: 'ANTIPASCHA', desc: this.t.services.serviceDescriptions.antipascha },
[14]: { id: 'myrrhbearers', name: this.t.services.serviceNames.myrrhbearers, type: 'MYRRHBEARERS', desc: this.t.services.serviceDescriptions.myrrhbearers },
[21]: { id: 'paralytic', name: this.t.services.serviceNames.paralytic, type: 'PARALYTIC', desc: this.t.services.serviceDescriptions.paralytic },
[25]: { id: 'prepolovenie', name: this.t.services.serviceNames.prepolovenie, type: 'PREPOLOVENIE', desc: this.t.services.serviceDescriptions.prepolovenie },
[28]: { id: 'samaritan', name: this.t.services.serviceNames.samaritan, type: 'SAMARITAN', desc: this.t.services.serviceDescriptions.samaritan },
[35]: { id: 'blindman', name: this.t.services.serviceNames.blindman, type: 'BLINDMAN', desc: this.t.services.serviceDescriptions.blindman },
[38]: { id: 'apodosis', name: this.t.services.serviceNames.apodosis, type: 'APODOSIS', desc: this.t.services.serviceDescriptions.apodosis },
[39]: { id: 'ascension', name: this.t.services.serviceNames.ascension, type: 'ASCENSION', desc: this.t.services.serviceDescriptions.ascension },
[42]: { id: 'holyfathers', name: this.t.services.serviceNames.holyfathers, type: 'HOLYFATHERS', desc: this.t.services.serviceDescriptions.holyfathers },
[48]: { id: 'pentecostsaturday', name: this.t.services.serviceNames.pentecostsaturday, type: 'PENTECOSTSATURDAY', desc: this.t.services.serviceDescriptions.pentecostsaturday },
[49]: { id: 'pentecost', name: this.t.services.serviceNames.pentecost, type: 'PENTECOST', desc: this.t.services.serviceDescriptions.pentecost },
[50]: { id: 'holyspirit', name: this.t.services.serviceNames.holyspirit, type: 'HOLYSPIRIT', desc: this.t.services.serviceDescriptions.holyspirit },
[56]: { id: 'allsaints', name: this.t.services.serviceNames.allsaints, type: 'ALLSAINTS', desc: this.t.services.serviceDescriptions.allsaints },
};
const pd = pentDay[dayInfo.nday];
if (pd) {
const templateName = pd.id.charAt(0).toUpperCase() + pd.id.slice(1);
special.push({
id: pd.id, name: pd.name, serviceType: pd.type,
template: templateName,
description: pd.desc,
titleNode: title(`${templateName}1`, pd.name),
});
}
// All Saints of Russia (appendix, same Sunday as All Saints)
if (dayInfo.nday === 56) {
special.push({
id: 'russiansaints', name: this.t.services.serviceNames.russiansaints, serviceType: 'RUSSIANSAINTS',
template: 'RussianSaints',
description: this.t.services.serviceDescriptions.russiansaints,
titleNode: title('RussianSaints1', this.t.services.serviceNames.russiansaints),
});
}
}
// First Week of Lent: complete daily cycle (Matins, Hours, Typica, Vespers, Compline)
if (dayInfo.isLent && dayInfo.nday >= -48 && dayInfo.nday <= -43) {
const fwDay: Record<number, { id: string; name: string; type: string; desc: string }> = {
[-48]: { id: 'firstweekmonday', name: this.t.services.serviceNames.firstweekmonday, type: 'FIRSTWEEKMONDAY', desc: this.t.services.serviceDescriptions.firstweekmonday },
[-47]: { id: 'firstweektuesday', name: this.t.services.serviceNames.firstweektuesday, type: 'FIRSTWEEKTUESDAY', desc: this.t.services.serviceDescriptions.firstweektuesday },
[-46]: { id: 'firstweekwednesday', name: this.t.services.serviceNames.firstweekwednesday, type: 'FIRSTWEEKWEDNESDAY', desc: this.t.services.serviceDescriptions.firstweekwednesday },
[-45]: { id: 'firstweekthursday', name: this.t.services.serviceNames.firstweekthursday, type: 'FIRSTWEEKTHURSDAY', desc: this.t.services.serviceDescriptions.firstweekthursday },
[-44]: { id: 'firstweekfriday', name: this.t.services.serviceNames.firstweekfriday, type: 'FIRSTWEEKFRIDAY', desc: this.t.services.serviceDescriptions.firstweekfriday },
[-43]: { id: 'firstweeksaturday', name: this.t.services.serviceNames.firstweeksaturday, type: 'FIRSTWEEKSATURDAY', desc: this.t.services.serviceDescriptions.firstweeksaturday },
};
const fw = fwDay[dayInfo.nday];
if (fw) {
const templateName = 'FirstWeek' + fw.id.charAt(0).toUpperCase() + fw.id.slice(1);
special.push({
id: fw.id, name: fw.name, serviceType: fw.type,
template: templateName,
description: fw.desc,
titleNode: title(`${templateName}1`, fw.name),
});
}
}
// Festal Menaion: Great feasts on fixed Julian dates (Typikon Ch. 47)
const menaionFeast: Record<string, { id: string; name: string; type: string; desc: string }> = {
'9-8': { id: 'nativitytheotokos', name: this.t.services.serviceNames.nativitytheotokos, type: 'NATIVITYTHEOTOKOS', desc: this.t.services.serviceDescriptions.nativitytheotokos },
'9-14': { id: 'exaltation', name: this.t.services.serviceNames.exaltation, type: 'EXALTATION', desc: this.t.services.serviceDescriptions.exaltation },
'11-21': { id: 'vvedenie', name: this.t.services.serviceNames.vvedenie, type: 'VVEDENIE', desc: this.t.services.serviceDescriptions.vvedenie },
'12-24': { id: 'nativityhours', name: this.t.services.serviceNames.nativityhours, type: 'NATIVITYHOURS', desc: this.t.services.serviceDescriptions.nativityhours },
'12-25': { id: 'nativity', name: this.t.services.serviceNames.nativity, type: 'NATIVITY', desc: this.t.services.serviceDescriptions.nativity },
'1-5': { id: 'theophanyhours', name: this.t.services.serviceNames.theophanyhours, type: 'THEOPHANYHOURS', desc: this.t.services.serviceDescriptions.theophanyhours },
'1-6': { id: 'theophany', name: this.t.services.serviceNames.theophany, type: 'THEOPHANY', desc: this.t.services.serviceDescriptions.theophany },
'2-2': { id: 'sretenie', name: this.t.services.serviceNames.sretenie, type: 'SRETENIE', desc: this.t.services.serviceDescriptions.sretenie },
'3-25': { id: 'annunciation', name: this.t.services.serviceNames.annunciation, type: 'ANNUNCIATION', desc: this.t.services.serviceDescriptions.annunciation },
'6-24': { id: 'forerunnerbirth', name: this.t.services.serviceNames.forerunnerbirth, type: 'FORERUNNERBIRTH', desc: this.t.services.serviceDescriptions.forerunnerbirth },
'6-29': { id: 'peterpaul', name: this.t.services.serviceNames.peterpaul, type: 'PETERPAUL', desc: this.t.services.serviceDescriptions.peterpaul },
'8-6': { id: 'transfiguration', name: this.t.services.serviceNames.transfiguration, type: 'TRANSFIGURATION', desc: this.t.services.serviceDescriptions.transfiguration },
'8-15': { id: 'dormition', name: this.t.services.serviceNames.dormition, type: 'DORMITION', desc: this.t.services.serviceDescriptions.dormition },
'8-29': { id: 'forerunnerbeheading', name: this.t.services.serviceNames.forerunnerbeheading, type: 'FORERUNNERBEHEADING', desc: this.t.services.serviceDescriptions.forerunnerbeheading },
// Middle feasts (fixed dates)
'9-25': { id: 'sergius', name: this.t.services.serviceNames.sergius, type: 'SERGIUS', desc: this.t.services.serviceDescriptions.sergius },
'9-26': { id: 'johntheologiansep', name: this.t.services.serviceNames.johntheologiansep, type: 'JOHNTHEOLOGIANSEP', desc: this.t.services.serviceDescriptions.johntheologiansep },
'10-1': { id: 'pokrov', name: this.t.services.serviceNames.pokrov, type: 'POKROV', desc: this.t.services.serviceDescriptions.pokrov },
'10-10': { id: 'ambrose', name: this.t.services.serviceNames.ambrose, type: 'AMBROSE', desc: this.t.services.serviceDescriptions.ambrose },
'10-11': { id: 'seventhcouncilefathers', name: this.t.services.serviceNames.seventhcouncilefathers, type: 'SEVENTHCOUNCILFATHERS', desc: this.t.services.serviceDescriptions.seventhcouncilefathers },
'10-22': { id: 'kazan', name: this.t.services.serviceNames.kazan, type: 'KAZAN', desc: this.t.services.serviceDescriptions.kazan },
'10-26': { id: 'demetrius', name: this.t.services.serviceNames.demetrius, type: 'DEMETRIUS', desc: this.t.services.serviceDescriptions.demetrius },
'11-8': { id: 'michaelsynaxis', name: this.t.services.serviceNames.michaelsynaxis, type: 'MICHAELSYNAXIS', desc: this.t.services.serviceDescriptions.michaelsynaxis },
'12-6': { id: 'nicholas', name: this.t.services.serviceNames.nicholas, type: 'NICHOLAS', desc: this.t.services.serviceDescriptions.nicholas },
'1-1': { id: 'circumcision', name: this.t.services.serviceNames.circumcision, type: 'CIRCUMCISION', desc: this.t.services.serviceDescriptions.circumcision },
'2-24': { id: 'findinghead1st', name: this.t.services.serviceNames.findinghead1st, type: 'FINDINGHEAD1ST', desc: this.t.services.serviceDescriptions.findinghead1st },
'3-9': { id: 'fortymartyrs', name: this.t.services.serviceNames.fortymartyrs, type: 'FORTYMARTYRS', desc: this.t.services.serviceDescriptions.fortymartyrs },
'5-8': { id: 'johntheologianmay', name: this.t.services.serviceNames.johntheologianmay, type: 'JOHNTHEOLOGIANMAY', desc: this.t.services.serviceDescriptions.johntheologianmay },
'5-9': { id: 'nicholastranslation', name: this.t.services.serviceNames.nicholastranslation, type: 'NICHOLASTRANSLATION', desc: this.t.services.serviceDescriptions.nicholastranslation },
'5-25': { id: 'findinghead3rd', name: this.t.services.serviceNames.findinghead3rd, type: 'FINDINGHEAD3RD', desc: this.t.services.serviceDescriptions.findinghead3rd },
'7-15': { id: 'vladimir', name: this.t.services.serviceNames.vladimir, type: 'VLADIMIR', desc: this.t.services.serviceDescriptions.vladimir },
'7-16': { id: 'sixcouncilfathers', name: this.t.services.serviceNames.sixcouncilfathers, type: 'SIXCOUNCILFATHERS', desc: this.t.services.serviceDescriptions.sixcouncilfathers },
'7-20': { id: 'elijah', name: this.t.services.serviceNames.elijah, type: 'ELIJAH', desc: this.t.services.serviceDescriptions.elijah },
'7-27': { id: 'panteleimon', name: this.t.services.serviceNames.panteleimon, type: 'PANTELEIMON', desc: this.t.services.serviceDescriptions.panteleimon },
'8-1': { id: 'processioncross', name: this.t.services.serviceNames.processioncross, type: 'PROCESSIONCROSS', desc: this.t.services.serviceDescriptions.processioncross },
};
const feast = menaionFeast[`${dayInfo.month}-${dayInfo.day}`];
if (feast) {
const templateName = feast.id.charAt(0).toUpperCase() + feast.id.slice(1);
special.push({
id: feast.id, name: feast.name, serviceType: feast.type,
template: templateName,
description: feast.desc,
titleNode: title(`${templateName}1`, feast.name),
});
}
// Movable Sundays of the Nativity season (dow-based)
if (dayInfo.dow === 0 && dayInfo.month === 12) {
const movable: { id: string; name: string; type: string; desc: string }[] = [];
if (dayInfo.day >= 11 && dayInfo.day <= 17) {
movable.push({ id: 'forefatherssunday', name: this.t.services.serviceNames.forefatherssunday, type: 'FOREFATHERSSUNDAY', desc: this.t.services.serviceDescriptions.forefatherssunday });
} else if (dayInfo.day >= 18 && dayInfo.day <= 24) {
movable.push({ id: 'holyfathersnativity', name: this.t.services.serviceNames.holyfathersnativity, type: 'HOLYFATHERSNATIVITY', desc: this.t.services.serviceDescriptions.holyfathersnativity });
} else if (dayInfo.day >= 26 && dayInfo.day <= 31) {
movable.push({ id: 'sundayafternativity', name: this.t.services.serviceNames.sundayafternativity, type: 'SUNDAYAFTERNATIVITY', desc: this.t.services.serviceDescriptions.sundayafternativity });
}
const mv = movable[0];
if (mv) {
const templateName = mv.id.charAt(0).toUpperCase() + mv.id.slice(1);
special.push({
id: mv.id, name: mv.name, serviceType: mv.type,
template: templateName,
description: mv.desc,
titleNode: title(`${templateName}1`, mv.name),
});
}
}
// Great Compline: Lenten weekdays (Mon-Thu)
if (dayInfo.isLent && dayInfo.dow >= 1 && dayInfo.dow <= 4) {
services.push({
special.push({
id: 'greatcompline', name: this.t.services.serviceNames.greatcompline, serviceType: 'GREATCOMPLINE',
template: 'GreatCompline',
description: this.t.services.serviceDescriptions.greatcompline,
@@ -196,7 +415,7 @@ export class ServiceView {
// Standing of St. Mary of Egypt: Thursday of the 5th week of Great Lent
if (dayInfo.isLent && dayInfo.nday === -17) {
services.push({
special.push({
id: 'mariasstanding', name: this.t.services.serviceNames.mariasstanding, serviceType: 'MARIASSTANDING',
template: 'MariasStanding',
description: this.t.services.serviceDescriptions.mariasstanding,
@@ -204,7 +423,96 @@ export class ServiceView {
});
}
return services;
// Holy Week services
if (dayInfo.isLent) {
// Palm Sunday: Vespers + Matins
if (dayInfo.nday === -7) {
special.push({
id: 'palmsunday', name: this.t.services.serviceNames.palmsunday, serviceType: 'PALMSUNDAY',
template: 'PalmSunday',
description: this.t.services.serviceDescriptions.palmsunday,
titleNode: title('PalmSunday1', this.t.services.serviceNames.palmsunday),
});
}
// Great Monday through Thursday: daily cycle (Matins, Hours, Typica, Vespers)
const greatDay: Record<number, { id: string; name: string; type: string; desc: string }> = {
[-6]: { id: 'greatmonday', name: this.t.services.serviceNames.greatmonday, type: 'GREATMONDAY', desc: this.t.services.serviceDescriptions.greatmonday },
[-5]: { id: 'greattuesday', name: this.t.services.serviceNames.greattuesday, type: 'GREATTUESDAY', desc: this.t.services.serviceDescriptions.greattuesday },
[-4]: { id: 'greatwednesday', name: this.t.services.serviceNames.greatwednesday, type: 'GREATWEDNESDAY', desc: this.t.services.serviceDescriptions.greatwednesday },
[-3]: { id: 'greatthursday', name: this.t.services.serviceNames.greatthursday, type: 'GREATTHURSDAY', desc: this.t.services.serviceDescriptions.greatthursday },
};
const gd = greatDay[dayInfo.nday];
if (gd) {
special.push({
id: gd.id, name: gd.name, serviceType: gd.type,
template: gd.id.charAt(0).toUpperCase() + gd.id.slice(1),
description: gd.desc,
titleNode: title(`${gd.id.charAt(0).toUpperCase() + gd.id.slice(1)}1`, gd.name),
});
}
// Matins of the 12 Passion Gospels: Great Thursday evening (Great Friday Matins)
if (dayInfo.nday === -3) {
special.push({
id: 'passiongospels', name: this.t.services.serviceNames.passiongospels, serviceType: 'PASSIONGOSPELS',
template: 'PassionGospels',
description: this.t.services.serviceDescriptions.passiongospels,
titleNode: title('PassionGospels1', this.t.services.serviceNames.passiongospels),
});
}
// Royal Hours of Great Friday
if (dayInfo.nday === -2) {
special.push({
id: 'royalhoursfriday', name: this.t.services.serviceNames.royalhoursfriday, serviceType: 'ROYALHOURSFRIDAY',
template: 'RoyalHoursFriday',
description: this.t.services.serviceDescriptions.royalhoursfriday,
titleNode: title('RoyalHoursFriday1', this.t.services.serviceNames.royalhoursfriday),
});
}
// Lamentations Matins: Great Saturday Matins (served Friday night)
if (dayInfo.nday === -1) {
special.push({
id: 'lamentations', name: this.t.services.serviceNames.lamentations, serviceType: 'LAMENTATIONS',
template: 'Lamentations',
description: this.t.services.serviceDescriptions.lamentations,
titleNode: title('Lamentations1', this.t.services.serviceNames.lamentations),
});
}
// Vespers of the Burial + cell Compline: Great Friday evening
if (dayInfo.nday === -2) {
special.push({
id: 'burialvespers', name: this.t.services.serviceNames.burialvespers, serviceType: 'BURIALVESPERS',
template: 'BurialVespers',
description: this.t.services.serviceDescriptions.burialvespers,
titleNode: title('BurialVespers1', this.t.services.serviceNames.burialvespers),
});
}
// Holy Saturday: Hours, Vespers+Liturgy of St. Basil, Midnight Office
if (dayInfo.nday === -1) {
special.push({
id: 'saturdayhours', name: this.t.services.serviceNames.saturdayhours, serviceType: 'SATURDAYHOURS',
template: 'SaturdayHours',
description: this.t.services.serviceDescriptions.saturdayhours,
titleNode: title('SaturdayHours1', this.t.services.serviceNames.saturdayhours),
});
special.push({
id: 'saturdayliturgy', name: this.t.services.serviceNames.saturdayliturgy, serviceType: 'SATURDAYLITURGY',
template: 'SaturdayVespersLiturgy',
description: this.t.services.serviceDescriptions.saturdayliturgy,
titleNode: title('SaturdayVespersLiturgy1', this.t.services.serviceNames.saturdayliturgy),
});
special.push({
id: 'saturdaymidnight', name: this.t.services.serviceNames.saturdaymidnight, serviceType: 'SATURDAYMIDNIGHT',
template: 'SaturdayMidnight',
description: this.t.services.serviceDescriptions.saturdaymidnight,
titleNode: title('SaturdayMidnight1', this.t.services.serviceNames.saturdaymidnight),
});
}
}
if (special.length > 0 && daily.length > 0) {
daily[0].divider = true;
}
return [...special, ...daily];
}
private async loadToneData(serviceType: string): Promise<{ troparion1?: string; kontakion1?: string; theotokion1?: string; prokimenon1?: string; alleluia1?: string; hypakoe1?: string } | null> {
@@ -376,6 +684,46 @@ export class ServiceView {
}
}
private async loadParacleteCanon(tone: number, dow: number): Promise<ServiceNode[] | null> {
if (tone < 1 || tone > 8) return null;
const WEEKDAY = ['', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const dayKey = WEEKDAY[dow] ?? '';
if (!dayKey) return null;
const url = `/data/shared/services/canons/paraclete/tone${tone}/${dayKey}.json`;
try {
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> {
const url = `/data/shared/menaion-daily/${dateKey}/${index}.json`;
try {
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> {
const url = `/data/shared/triodion/${String(index).padStart(2, '0')}.json`;
try {
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> {
const url = `/data/shared/services/marias-standing/full.json`;
try {
@@ -388,6 +736,18 @@ export class ServiceView {
}
}
private async loadHolyWeekData(service: string): Promise<ServiceNode[] | null> {
const url = `/data/shared/services/${service}/full.json`;
try {
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> {
const computed = computeDay(this.currentDate);
const { dayInfo } = computed;
@@ -464,7 +824,7 @@ export class ServiceView {
private generateVarNodes(toneData: { troparion1?: string; kontakion1?: string; theotokion1?: string; prokimenon1?: string; alleluia1?: string; hypakoe1?: string } | null, serviceType: string): Map<string, ServiceNode[]> {
const varNodes = new Map<string, ServiceNode[]>();
const prefix: Record<string, string> = { PRIMES: '', TERCE: '3', SEXTE: '6', NONE: '9', VESPERS: 'V', LITURGY: 'L', MATINS: 'U', GREATCOMPLINE: 'GC', MARIASSTANDING: 'MS' };
const prefix: Record<string, string> = { PRIMES: '', TERCE: '3', SEXTE: '6', NONE: '9', VESPERS: 'V', LITURGY: 'L', MATINS: 'U', GREATCOMPLINE: 'GC', MARIASSTANDING: 'MS', PASSIONGOSPELS: 'PG', ROYALHOURSFRIDAY: 'RF', LAMENTATIONS: 'LM', PALMSUNDAY: 'PS', GREATMONDAY: 'GM', GREATTUESDAY: 'GT', GREATWEDNESDAY: 'GW', GREATTHURSDAY: 'GTH', BURIALVESPERS: 'BV', SATURDAYHOURS: 'SH', SATURDAYLITURGY: 'SL', SATURDAYMIDNIGHT: 'SM', PASCHA: 'PA', BRIGHTMONDAY: 'BM', BRIGHTTUESDAY: 'BT', BRIGHTWEDNESDAY: 'BW', BRIGHTTHURSDAY: 'BTH', BRIGHTFRIDAY: 'BF', BRIGHTSATURDAY: 'BS', ANTIPASCHA: 'AP', MYRRHBEARERS: 'MB', PARALYTIC: 'PL', PREPOLOVENIE: 'PP', SAMARITAN: 'SA', BLINDMAN: 'BM', APODOSIS: 'AD', ASCENSION: 'AS', HOLYFATHERS: 'HF', PENTECOSTSATURDAY: 'PSA', PENTECOST: 'PE', HOLYSPIRIT: 'HS', ALLSAINTS: 'AL', RUSSIANSAINTS: 'RS', FIRSTWEEKMONDAY: 'FM', FIRSTWEEKTUESDAY: 'FT', FIRSTWEEKWEDNESDAY: 'FW', FIRSTWEEKTHURSDAY: 'FTH', FIRSTWEEKFRIDAY: 'FF', FIRSTWEEKSATURDAY: 'FS', NATIVITYTHEOTOKOS: 'NT', EXALTATION: 'EX', VVEDENIE: 'VV', NATIVITYHOURS: 'NH', NATIVITY: 'NA', THEOPHANYHOURS: 'TH', THEOPHANY: 'TP', SRETENIE: 'SR', ANNUNCIATION: 'AN', FORERUNNERBIRTH: 'FB', PETERPAUL: 'PP', TRANSFIGURATION: 'TR', DORMITION: 'DO', FORERUNNERBEHEADING: 'FH', SERGIUS: 'SE', JOHNTHEOLOGIANSEP: 'JT', POKROV: 'PO', AMBROSE: 'AM', SEVENTHCOUNCILFATHERS: 'SC', KAZAN: 'KZ', DEMETRIUS: 'DE', MICHAELSYNAXIS: 'MS', NICHOLAS: 'NI', CIRCUMCISION: 'CI', FINDINGHEAD1ST: 'F1', FORTYMARTYRS: 'FM', JOHNTHEOLOGIANMAY: 'JM', NICHOLASTRANSLATION: 'NT', FINDINGHEAD3RD: 'F3', VLADIMIR: 'VL', SIXCOUNCILFATHERS: 'XC', ELIJAH: 'EL', PANTELEIMON: 'PA', PROCESSIONCROSS: 'PC', FOREFATHERSSUNDAY: 'FD', HOLYFATHERSNATIVITY: 'HFN', SUNDAYAFTERNATIVITY: 'SAN' };
const p = prefix[serviceType] ?? '';
if (toneData?.troparion1) {
@@ -593,11 +953,37 @@ export class ServiceView {
// Load Matins canon data
if (service.serviceType === 'MATINS') {
const computed = computeDay(this.currentDate);
const canonNodes = await this.loadCanonData(computed.dayInfo.Tone);
const tone = computed.dayInfo.Tone;
const dow = computed.dayInfo.dow;
// Weekdays (Mon-Sat): use the Параклитика weekday canons; Sunday: Octoechos Sunday canon
if (dow >= 1 && dow <= 6) {
const paracleteNodes = await this.loadParacleteCanon(tone, dow);
if (paracleteNodes) {
varNodes.set('PCanonU', paracleteNodes);
}
} else {
const canonNodes = await this.loadCanonData(tone);
if (canonNodes) {
varNodes.set('PCanonU', canonNodes);
}
}
}
// Load Daily Menaion service (Служба дня)
if (service.serviceType === 'MENAIONDAY' && service.menaionDateKey && service.menaionDayIndex) {
const dayNodes = await this.loadMenaionDay(service.menaionDateKey, service.menaionDayIndex);
if (dayNodes) {
varNodes.set('PMenaionDay', dayNodes);
}
}
// Load Triodion movable-day service
if (service.serviceType === 'TRIODION' && service.triodionIndex) {
const triodionNodes = await this.loadTriodion(service.triodionIndex);
if (triodionNodes) {
varNodes.set('PTriodion', triodionNodes);
}
}
// Load Great Canon data for Great Compline
if (service.serviceType === 'GREATCOMPLINE') {
@@ -615,6 +1001,169 @@ export class ServiceView {
}
}
// Load Holy Week service data
const holyWeekVarMap: Record<string, string> = {
PALMSUNDAY: 'PPalmSunday',
GREATMONDAY: 'PGreatMonday',
GREATTUESDAY: 'PGreatTuesday',
GREATWEDNESDAY: 'PGreatWednesday',
GREATTHURSDAY: 'PGreatThursday',
PASSIONGOSPELS: 'PPassionGospels',
ROYALHOURSFRIDAY: 'PRoyalHoursFriday',
LAMENTATIONS: 'PLamentations',
BURIALVESPERS: 'PBurialVespers',
SATURDAYHOURS: 'PSaturdayHours',
SATURDAYLITURGY: 'PSaturdayVespersLiturgy',
SATURDAYMIDNIGHT: 'PSaturdayMidnight',
PASCHA: 'PPascha',
BRIGHTMONDAY: 'PBrightMonday',
BRIGHTTUESDAY: 'PBrightTuesday',
BRIGHTWEDNESDAY: 'PBrightWednesday',
BRIGHTTHURSDAY: 'PBrightThursday',
BRIGHTFRIDAY: 'PBrightFriday',
BRIGHTSATURDAY: 'PBrightSaturday',
ANTIPASCHA: 'PAntipascha',
MYRRHBEARERS: 'PMyrrhbearers',
PARALYTIC: 'PParalytic',
PREPOLOVENIE: 'PPrepolovenie',
SAMARITAN: 'PSamaritan',
BLINDMAN: 'PBlindMan',
APODOSIS: 'PApodosis',
ASCENSION: 'PAscension',
HOLYFATHERS: 'PHolyFathers',
PENTECOSTSATURDAY: 'PPentecostSaturday',
PENTECOST: 'PPentecost',
HOLYSPIRIT: 'PHolySpirit',
ALLSAINTS: 'PAllSaints',
RUSSIANSAINTS: 'PRussianSaints',
FIRSTWEEKMONDAY: 'PFirstWeekMonday',
FIRSTWEEKTUESDAY: 'PFirstWeekTuesday',
FIRSTWEEKWEDNESDAY: 'PFirstWeekWednesday',
FIRSTWEEKTHURSDAY: 'PFirstWeekThursday',
FIRSTWEEKFRIDAY: 'PFirstWeekFriday',
FIRSTWEEKSATURDAY: 'PFirstWeekSaturday',
NATIVITYTHEOTOKOS: 'PNativityTheotokos',
EXALTATION: 'PExaltation',
VVEDENIE: 'PVvedenie',
NATIVITYHOURS: 'PNativityHours',
NATIVITY: 'PNativity',
THEOPHANYHOURS: 'PTheophanyHours',
THEOPHANY: 'PTheophany',
SRETENIE: 'PSretenie',
ANNUNCIATION: 'PAnnunciation',
FORERUNNERBIRTH: 'PForerunnerBirth',
PETERPAUL: 'PPeterPaul',
TRANSFIGURATION: 'PTransfiguration',
DORMITION: 'PDormition',
FORERUNNERBEHEADING: 'PForerunnerBeheading',
SERGIUS: 'PSergius',
JOHNTHEOLOGIANSEP: 'PJohnTheologianSep',
POKROV: 'PPokrov',
AMBROSE: 'PAmbrose',
SEVENTHCOUNCILFATHERS: 'PSeventhCouncilFathers',
KAZAN: 'PKazan',
DEMETRIUS: 'PDemetrius',
MICHAELSYNAXIS: 'PMichaelSynaxis',
NICHOLAS: 'PNicholas',
CIRCUMCISION: 'PCircumcision',
FINDINGHEAD1ST: 'PFindingHead1st',
FORTYMARTYRS: 'PFortyMartyrs',
JOHNTHEOLOGIANMAY: 'PJohnTheologianMay',
NICHOLASTRANSLATION: 'PNicholasTranslation',
FINDINGHEAD3RD: 'PFindingHead3rd',
VLADIMIR: 'PVladimir',
SIXCOUNCILFATHERS: 'PSixCouncilsFathers',
ELIJAH: 'PElijah',
PANTELEIMON: 'PPanteleimon',
PROCESSIONCROSS: 'PProcessionCross',
FOREFATHERSSUNDAY: 'PForefathersSunday',
HOLYFATHERSNATIVITY: 'PHolyFathersNativity',
SUNDAYAFTERNATIVITY: 'PSundayAfterNativity',
};
const holyWeekDir: Record<string, string> = {
PALMSUNDAY: 'palm-sunday',
GREATMONDAY: 'great-monday',
GREATTUESDAY: 'great-tuesday',
GREATWEDNESDAY: 'great-wednesday',
GREATTHURSDAY: 'great-thursday',
PASSIONGOSPELS: 'passion-gospels',
ROYALHOURSFRIDAY: 'royal-hours-friday',
LAMENTATIONS: 'lamentations',
BURIALVESPERS: 'burial-vespers',
SATURDAYHOURS: 'saturday-hours',
SATURDAYLITURGY: 'saturday-vespers-liturgy',
SATURDAYMIDNIGHT: 'saturday-midnight',
PASCHA: 'pascha',
BRIGHTMONDAY: 'bright-monday',
BRIGHTTUESDAY: 'bright-tuesday',
BRIGHTWEDNESDAY: 'bright-wednesday',
BRIGHTTHURSDAY: 'bright-thursday',
BRIGHTFRIDAY: 'bright-friday',
BRIGHTSATURDAY: 'bright-saturday',
ANTIPASCHA: 'antipascha',
MYRRHBEARERS: 'myrrhbearers',
PARALYTIC: 'paralytic',
PREPOLOVENIE: 'prepolovenie',
SAMARITAN: 'samaritan',
BLINDMAN: 'blindman',
APODOSIS: 'apodosis',
ASCENSION: 'ascension',
HOLYFATHERS: 'holyfathers',
PENTECOSTSATURDAY: 'pentecostsaturday',
PENTECOST: 'pentecost',
HOLYSPIRIT: 'holyspirit',
ALLSAINTS: 'allsaints',
RUSSIANSAINTS: 'russiansaints',
FIRSTWEEKMONDAY: 'first-week-monday',
FIRSTWEEKTUESDAY: 'first-week-tuesday',
FIRSTWEEKWEDNESDAY: 'first-week-wednesday',
FIRSTWEEKTHURSDAY: 'first-week-thursday',
FIRSTWEEKFRIDAY: 'first-week-friday',
FIRSTWEEKSATURDAY: 'first-week-saturday',
NATIVITYTHEOTOKOS: 'nativity-theotokos',
EXALTATION: 'exaltation',
VVEDENIE: 'vvedenie',
NATIVITYHOURS: 'nativity-hours',
NATIVITY: 'nativity',
THEOPHANYHOURS: 'theophany-hours',
THEOPHANY: 'theophany',
SRETENIE: 'sretenie',
ANNUNCIATION: 'annunciation',
FORERUNNERBIRTH: 'forerunner-birth',
PETERPAUL: 'peter-paul',
TRANSFIGURATION: 'transfiguration',
DORMITION: 'dormition',
FORERUNNERBEHEADING: 'forerunner-beheading',
SERGIUS: 'sergius',
JOHNTHEOLOGIANSEP: 'johntheologian-sep',
POKROV: 'pokrov',
AMBROSE: 'ambrose',
SEVENTHCOUNCILFATHERS: 'seventh-council-fathers',
KAZAN: 'kazan',
DEMETRIUS: 'demetrius',
MICHAELSYNAXIS: 'michael-synaxis',
NICHOLAS: 'nicholas',
CIRCUMCISION: 'circumcision',
FINDINGHEAD1ST: 'finding-head-1st',
FORTYMARTYRS: 'forty-martyrs',
JOHNTHEOLOGIANMAY: 'johntheologian-may',
NICHOLASTRANSLATION: 'nicholas-translation',
FINDINGHEAD3RD: 'finding-head-3rd',
VLADIMIR: 'vladimir',
SIXCOUNCILFATHERS: 'six-councils-fathers',
ELIJAH: 'elijah',
PANTELEIMON: 'panteleimon',
PROCESSIONCROSS: 'procession-cross',
FOREFATHERSSUNDAY: 'forefathers-sunday',
HOLYFATHERSNATIVITY: 'holy-fathers-nativity',
SUNDAYAFTERNATIVITY: 'sunday-after-nativity',
};
const hwVar = holyWeekVarMap[service.serviceType];
if (hwVar) {
const nodes = await this.loadHolyWeekData(holyWeekDir[service.serviceType]);
if (nodes) varNodes.set(hwVar, nodes);
}
// Load Epistle and Gospel readings from commemoration
const loadReadings = async () => {
const ids: string[] = [];