diff --git a/cantiletture.md b/cantiletture.md new file mode 100644 index 0000000..a42be59 --- /dev/null +++ b/cantiletture.md @@ -0,0 +1,224 @@ +# Piano di Azione: Suggerimento Canti con AI (cantiletture.json) + +## Obiettivo + +Generare automaticamente, una volta a settimana, un file JSON con i canti suggeriti per ogni messa della settimana, analizzati da Gemini Flash a partire dalle letture liturgiche. La PWA scarica questo file statico e mostra i suggerimenti istantaneamente, senza alcun calcolo lato client. + +--- + +## Architettura + +``` +CRON Settimanale (Lunedì ore 6:00) + │ + ├─ 1. Fetch RSS liturgia.silvestrini.org/rss + │ → Estrae le letture di tutta la settimana (7 giorni) + │ + ├─ 2. Fetch canti.json dal server + │ → Carica il database completo dei canti con testi e momenti liturgici + │ + ├─ 3. Chiamata API Gemini Flash + │ → Invia prompt strutturato con letture + lista canti + │ → Riceve suggerimenti ragionati con score e motivazione + │ + └─ 4. Salva cantiletture.json + → Upload sul server web (stessa posizione di canti.json) +``` + +--- + +## Struttura del File cantiletture.json + +```json +{ + "generated_at": "2026-05-19T06:00:00Z", + "week_start": "2026-05-17", + "week_end": "2026-05-24", + "masses": { + "2026-05-17": { + "title": "Ascensione del Signore", + "day": "Domenica", + "moments": { + "Ingresso": [ + { + "id_canti": 42, + "titolo": "Cristo è risorto veramente", + "score": 95, + "motivo": "Il tema dell'ascensione e della gloria di Cristo risorto è centrale in questo canto." + }, + { + "id_canti": 118, + "titolo": "Alleluia, è risorto", + "score": 88, + "motivo": "Il tono gioioso e la proclamazione pasquale si collegano al mistero celebrato." + }, + { + "id_canti": 205, + "titolo": "Canto di lode al Signore", + "score": 82, + "motivo": "Richiama il tema della lode che accompagna l'ascensione di Gesù." + } + ], + "Offertorio": [ + { "...": "3 canti suggeriti" } + ], + "Comunione": [ + { "...": "3 canti suggeriti" } + ], + "Finale": [ + { "...": "3 canti suggeriti" } + ] + } + }, + "2026-05-18": { + "title": "Lunedì della VII settimana di Pasqua", + "day": "Lunedì", + "moments": { "...": "stessa struttura" } + } + } +} +``` + +### Campi per ogni canto suggerito: +| Campo | Tipo | Descrizione | +|-------|------|-------------| +| `id_canti` | number | ID del canto nel database (per collegamento diretto al player) | +| `titolo` | string | Titolo del canto (per visualizzazione rapida) | +| `score` | number (0-100) | Grado di affinità/affidabilità del suggerimento | +| `motivo` | string | Spiegazione testuale del perché il canto è adatto | + +--- + +## Componenti da Sviluppare + +### 1. Script di Generazione (`scripts/generate-cantiletture.ts`) + +Script Node.js/TypeScript che: + +- **Input**: RSS feed + canti.json +- **Elaborazione**: + 1. Parsing XML del feed RSS (estrazione letture per data) + 2. Preparazione del prompt per Gemini con: testo letture + lista canti (id, titolo, testo, momenti liturgici) + 3. Chiamata API Gemini Flash con output JSON strutturato + 4. Validazione della risposta (verifica che gli ID canti esistano, score nel range) +- **Output**: File `cantiletture.json` + +**Prompt di esempio per Gemini:** +``` +Sei un esperto liturgista cattolico. Ti fornisco le letture della messa +e un database di canti liturgici con i relativi momenti (Ingresso, +Offertorio, Comunione, Finale, ecc.). + +Per ogni giorno della settimana e per ogni momento liturgico, suggerisci +i 3 canti più adatti. Per ogni suggerimento indica: +- L'ID del canto (id_canti) +- Uno score da 0 a 100 che indica il grado di attinenza +- Una breve motivazione (max 1 frase) + +Considera: temi teologici, periodo liturgico, tono emotivo, +corrispondenze tra letture e testi dei canti. + +LETTURE DELLA SETTIMANA: +[...testo letture...] + +DATABASE CANTI: +[...lista canti con id, titolo, testo, momenti...] + +Rispondi SOLO con JSON valido nel formato specificato. +``` + +### 2. Configurazione API Key + +- Ottenere API Key gratuita da [Google AI Studio](https://aistudio.google.com/apikey) +- Salvarla come variabile d'ambiente `GEMINI_API_KEY` +- Non committarla nel repository (usare `.env` in `.gitignore`) + +### 3. Aggiornamento PWA + +#### 3.1 Nuovo Service: `CantiLettureService` +```typescript +// Logica semplificata: +// - Scarica cantiletture.json dal server (con cache) +// - Espone i suggerimenti per data selezionata +// - Fallback al keyword matching se il JSON non è disponibile +``` + +#### 3.2 Aggiornamento LiturgiaPage +- Rimuovere il tasto "Analizza e Suggerisci Canti" (non serve più) +- Rimuovere la sezione "Parole Chiave Rilevate" (non serve più) +- Mostrare automaticamente i suggerimenti raggruppati per momento liturgico +- Ogni card mostra: titolo, score (barra o badge), motivazione dell'AI +- Click sulla card → apre il canto nel player + +#### 3.3 Fallback +Se `cantiletture.json` non è disponibile o la data non è coperta: +- Mostrare un messaggio "Suggerimenti AI non disponibili per questa data" +- Opzionalmente, offrire il keyword matching come alternativa + +### 4. Automazione (CRON) + +#### Opzione A: Script locale con crontab +```bash +# Ogni lunedì alle 6:00 +0 6 * * 1 cd /path/to/canti && node scripts/generate-cantiletture.js +``` + +#### Opzione B: GitHub Action (se il repo viene messo su GitHub) +```yaml +# .github/workflows/generate-suggestions.yml +name: Generate Canti Suggestions +on: + schedule: + - cron: '0 6 * * 1' # Ogni lunedì alle 6:00 UTC + workflow_dispatch: # Esecuzione manuale +``` + +#### Opzione C: Script manuale +```bash +# Eseguibile a mano quando si vuole aggiornare +./scripts/generate-cantiletture.sh +``` + +--- + +## Stima Costi + +| Voce | Valore | +|------|--------| +| Chiamate API/settimana | 1 (una sola chiamata copre tutta la settimana) | +| Token input (letture + ~500 canti) | ~15.000 token | +| Token output (JSON suggerimenti) | ~3.000 token | +| Costo per chiamata (Gemini Flash) | ~0.001€ | +| **Costo mensile stimato** | **< 0.01€** | +| Rientra nell'abbonamento Google One | ✅ Sì | + +--- + +## Piano di Esecuzione (Step by Step) + +### Fase 1: Script di Generazione +1. [ ] Creare `scripts/generate-cantiletture.ts` +2. [ ] Implementare il parsing RSS +3. [ ] Implementare la chiamata Gemini con prompt ottimizzato +4. [ ] Testare con le letture della settimana corrente +5. [ ] Validare il JSON generato + +### Fase 2: Integrazione PWA +6. [ ] Creare `CantiLettureService` che scarica e gestisce il JSON +7. [ ] Aggiornare `LiturgiaPage` per mostrare i suggerimenti AI +8. [ ] Implementare il fallback al keyword matching +9. [ ] Testare l'interfaccia completa + +### Fase 3: Automazione +10. [ ] Configurare il cron/script di esecuzione automatica +11. [ ] Testare il ciclo completo (generazione → upload → visualizzazione) +12. [ ] Documentare la procedura nel README + +--- + +## Note Tecniche + +- **Dimensione stimata del JSON**: ~50-100 KB per settimana (7 giorni × 4-5 momenti × 3 canti) +- **Cache**: Il JSON viene cachato in localStorage con TTL di 24 ore +- **Compatibilità**: Il file viene servito dallo stesso server HTTP della PWA, nessun problema CORS +- **Retrocompatibilità**: Se il file non esiste, la PWA continua a funzionare con il keyword matching diff --git a/liturgia_strategy.md b/liturgia_strategy.md new file mode 100644 index 0000000..e4b9b56 --- /dev/null +++ b/liturgia_strategy.md @@ -0,0 +1,40 @@ +# Strategia di Suggerimento Canti Liturgici + +Questo documento descrive l'approccio tecnico utilizzato per suggerire i canti attinenti alle letture del giorno nella PWA "CantiCristiani". + +## 1. Algoritmo di Analisi (Keyword Matching) + +Al momento **non viene utilizzata un'Intelligenza Artificiale generativa (LLM)** per due motivi principali: +- **Performance**: L'analisi avviene istantaneamente sul dispositivo dell'utente senza chiamate API esterne lente. +- **Privacy e Offline**: Il sistema può funzionare anche con una connettività limitata una volta scaricato il testo delle letture. + +### Fasi dell'Algoritmo: + +1. **Keyword Extraction**: + - Il testo di tutte le letture (Prima, Salmo, Seconda, Vangelo) viene normalizzato in minuscolo. + - Vengono rimosse le "stop words" (parole comuni come articoli, preposizioni, congiunzioni). + - Vengono estratte solo le parole con lunghezza superiore a 3 caratteri per evitare rumore. +2. **Ponderazione e Scoring**: + - Ogni canto nel database viene analizzato cercando le keyword estratte. + - **Titolo (Peso 10)**: Se una parola chiave appare nel titolo, il punteggio del canto aumenta drasticamente. + - **Testo/Lyrics (Peso 1)**: Ogni occorrenza di una parola chiave nel testo del canto aggiunge 1 punto al punteggio totale. +3. **Classificazione per Momento Liturgico**: + - I canti vengono raggruppati in base alle categorie liturgiche (Ingresso, Gloria, Offertorio, Comunione, ecc.) definite nel database. + - Il sistema seleziona i migliori 3 canti per ogni categoria per offrire una scelta bilanciata. + +## 2. Selezione della Data Predefinita + +L'applicazione calcola automaticamente la **prossima domenica** come data di default. Questo perché la maggior parte degli utenti utilizza la funzione per preparare la liturgia festiva imminente. + +## 3. Sviluppi Futuri: Ricerca Semantica (Embeddings) + +Per superare i limiti del matching testuale, la strategia futura prevede l'integrazione di tecniche di **Semantic Search** che possono essere eseguite direttamente nella PWA: + +### Architettura Proposta: +- **Pre-calcolo dei Vettori**: Generazione dei vettori (embeddings) per l'intero database dei canti. Questi dati verrebbero inclusi in una versione estesa del file `canti.json`. +- **Analisi On-Device**: Utilizzo di librerie matematiche leggere (come `similarity-js`) per calcolare la "distanza del coseno" tra il vettore della lettura e quelli dei canti. +- **Vantaggi**: Il sistema identificherebbe correlazioni concettuali (es. "Luce" -> "Mondo", "Pane" -> "Eucarestia") anche in assenza di corrispondenze testuali esatte. + +### Implementazione Tecnica: +1. Integrazione di un modello di embedding (es. `all-MiniLM-L6-v2`) via Web Workers per non bloccare l'interfaccia. +2. Memorizzazione locale dei vettori tramite IndexedDB per garantire performance elevate dopo il primo caricamento. diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index cd6f74a..64a0fa4 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,6 +27,10 @@ const routes: Routes = [ path: 'propose-canto', loadComponent: () => import('./pages/propose-canto/propose-canto.page').then( m => m.ProposeCantoPage) }, + { + path: 'liturgia', + loadChildren: () => import('./pages/liturgia/liturgia.module').then( m => m.LiturgiaPageModule) + }, ]; @NgModule({ diff --git a/src/app/pages/liturgia/liturgia.module.ts b/src/app/pages/liturgia/liturgia.module.ts new file mode 100644 index 0000000..0828855 --- /dev/null +++ b/src/app/pages/liturgia/liturgia.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { RouterModule } from '@angular/router'; +import { LiturgiaPage } from './liturgia.page'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + RouterModule.forChild([ + { + path: '', + component: LiturgiaPage + } + ]) + ], + declarations: [LiturgiaPage] +}) +export class LiturgiaPageModule {} diff --git a/src/app/pages/liturgia/liturgia.page.html b/src/app/pages/liturgia/liturgia.page.html new file mode 100644 index 0000000..3b1360e --- /dev/null +++ b/src/app/pages/liturgia/liturgia.page.html @@ -0,0 +1,71 @@ + + + + + + Liturgia del Giorno + + + + +
+ +
+
+
+ + + +

{{ getTodayString() }}

+ + + +
+
+
+ +
+ +

Caricamento letture...

+
+ +
+ + + + + + + Prima Lettura + +
+
+ + + + + Seconda Lettura + +
+
+ + + + + Vangelo + +
+
+ +
+ + +
+ +
+

Non è stato possibile caricare le letture per oggi.

+ Riprova +
+ +
+
diff --git a/src/app/pages/liturgia/liturgia.page.scss b/src/app/pages/liturgia/liturgia.page.scss new file mode 100644 index 0000000..d6f6c10 --- /dev/null +++ b/src/app/pages/liturgia/liturgia.page.scss @@ -0,0 +1,144 @@ +.bg-gradient { + --background: transparent; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); +} + +.liturgia-container { + max-width: 800px; + margin: 0 auto; +} + +.glass { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + padding: 16px; +} + +.date-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + .date-navigation { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + h2 { + margin: 0; + font-size: 1.1rem; + color: var(--ion-color-secondary); + text-transform: capitalize; + text-align: center; + } + } +} + +.keywords-container { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 16px; +} + +.moment-group { + .moment-header { + margin-bottom: 8px; + ion-badge { + font-size: 0.9rem; + padding: 6px 12px; + border-radius: 8px; + } + } +} + +.glass-accordion { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + overflow: hidden; + + ion-accordion { + --background: transparent; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + &:last-child { + border-bottom: none; + } + + .reading-title { + color: var(--ion-color-secondary); + font-size: 1.1rem; + font-weight: 600; + margin: 0; + } + + .reading-text { + color: rgba(255, 255, 255, 0.9); + line-height: 1.6; + font-size: 1rem; + white-space: pre-wrap; + background: rgba(0, 0, 0, 0.2); + + ::ng-deep br { + display: block; + content: ""; + margin-top: 10px; + } + } + } +} + +.section-title { + color: #fff; + font-size: 1.2rem; + margin-bottom: 16px; +} + +.song-card { + padding: 0; + transition: transform 0.2s ease; + + &:active { + transform: scale(0.98); + } + + .song-title { + color: #fff; + font-weight: 600; + } + + .song-author { + color: rgba(255, 255, 255, 0.5); + font-size: 0.85rem; + } +} + +.transparent-item { + --background: transparent; + --color: #fff; + --padding-start: 16px; + --padding-end: 16px; +} + +/* Alto Contrasto per Liturgia */ +:host-context(body.high-contrast) { + .glass-accordion { + // Mantengo il look glass per gli header come richiesto + + ion-accordion { + .reading-text { + color: #000 !important; // Testo nero + background: #fff !important; // Sfondo bianco solido per massimo contrasto + padding: 20px !important; + } + } + } +} diff --git a/src/app/pages/liturgia/liturgia.page.ts b/src/app/pages/liturgia/liturgia.page.ts new file mode 100644 index 0000000..4b67ca1 --- /dev/null +++ b/src/app/pages/liturgia/liturgia.page.ts @@ -0,0 +1,42 @@ +import { Component, OnInit, inject, signal } from '@angular/core'; +import { LiturgyService } from '../../services/liturgy.service'; +import { Router } from '@angular/router'; +import { Canto } from '../../services/canti.service'; + +@Component({ + selector: 'app-liturgia', + templateUrl: './liturgia.page.html', + styleUrls: ['./liturgia.page.scss'], + standalone: false +}) +export class LiturgiaPage implements OnInit { + public liturgyService = inject(LiturgyService); + private router = inject(Router); + + ngOnInit() { + this.liturgyService.fetchTodayLiturgy(); + } + + suggest() { + this.liturgyService.suggestSongs(); + } + + onDateChange(event: any) { + const newDate = event.detail.value.split('T')[0]; + this.liturgyService.fetchTodayLiturgy(newDate); + } + + openSong(song: Canto) { + this.router.navigate(['/player'], { queryParams: { id: song.id } }); + } + + getTodayString(): string { + const date = new Date(this.liturgyService.selectedDate()); + return date.toLocaleDateString('it-IT', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + }); + } +} diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index 5f1e29a..568b878 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -57,7 +57,14 @@

Editor e Canti Personali

Abilita aggiunta e gestione "Miei"

- + + + + + +

Liturgia del Giorno

+

Letture e suggerimenti canti

+
diff --git a/src/app/services/liturgy.service.ts b/src/app/services/liturgy.service.ts new file mode 100644 index 0000000..96ed8ba --- /dev/null +++ b/src/app/services/liturgy.service.ts @@ -0,0 +1,173 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { CantiService, Canto } from './canti.service'; +import { firstValueFrom } from 'rxjs'; + +export interface LiturgyReadings { + date: string; + primaLettura?: string; + salmo?: string; + secondaLettura?: string; + vangelo?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class LiturgyService { + private http = inject(HttpClient); + private cantiService = inject(CantiService); + + public readings = signal(null); + public suggestions = signal([]); + public loading = signal(false); + + public selectedDate = signal(this.getNextSunday()); + public keywords = signal([]); + public groupedSuggestions = signal<{moment: string, canti: Canto[]}[]>([]); + + private PROXY_URL = 'https://corsproxy.io/?'; + private RSS_URL = 'https://liturgia.silvestrini.org/rss'; + + private getNextSunday(): string { + const d = new Date(); + d.setDate(d.getDate() + (7 - d.getDay()) % 7); + if (d.getDay() === 0 && new Date().getDay() === 0) { + // Keep today if today is Sunday + } else if (d.getDay() === 0) { + // already sunday + } else { + d.setDate(d.getDate() + (7 - d.getDay())); + } + return d.toISOString().split('T')[0]; + } + + prevDay() { + const d = new Date(this.selectedDate()); + d.setDate(d.getDate() - 1); + this.fetchTodayLiturgy(d.toISOString().split('T')[0]); + } + + nextDay() { + const d = new Date(this.selectedDate()); + d.setDate(d.getDate() + 1); + this.fetchTodayLiturgy(d.toISOString().split('T')[0]); + } + + async fetchTodayLiturgy(date?: string) { + if (date) this.selectedDate.set(date); + const targetDate = this.selectedDate(); + + this.loading.set(true); + this.suggestions.set([]); + this.groupedSuggestions.set([]); + this.keywords.set([]); + + try { + let xmlText = ''; + try { + xmlText = await firstValueFrom(this.http.get(this.PROXY_URL + encodeURIComponent(this.RSS_URL), { responseType: 'text' })); + } catch (e) { + const fallbackProxy = 'https://api.allorigins.win/raw?url='; + xmlText = await firstValueFrom(this.http.get(fallbackProxy + encodeURIComponent(this.RSS_URL), { responseType: 'text' })); + } + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); + const items = Array.from(xmlDoc.querySelectorAll('item')); + + const lettureItem = items.find(item => { + const title = item.querySelector('title')?.textContent || ''; + const category = item.querySelector('category')?.textContent || ''; + return title.startsWith(targetDate) && category === 'letture'; + }); + + if (lettureItem) { + const description = lettureItem.querySelector('description')?.textContent || ''; + this.readings.set(this.parseDescription(description, targetDate)); + } else { + this.readings.set(null); + } + } catch (err) { + console.error('Failed to fetch liturgy', err); + this.readings.set(null); + } finally { + this.loading.set(false); + } + } + + private parseDescription(html: string, date: string): LiturgyReadings { + const readings: LiturgyReadings = { date }; + + const parts = html.split(/([A-Z\s]+:)/); + + let currentLabel = ''; + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed.endsWith(':')) { + currentLabel = trimmed.toUpperCase(); + } else if (trimmed) { + if (currentLabel.includes('PRIMA LETTURA')) readings.primaLettura = trimmed; + else if (currentLabel.includes('SALMO')) readings.salmo = trimmed; + else if (currentLabel.includes('SECONDA LETTURA')) readings.secondaLettura = trimmed; + else if (currentLabel.includes('VANGELO')) readings.vangelo = trimmed; + } + } + + return readings; + } + + suggestSongs() { + const currentReadings = this.readings(); + if (!currentReadings) return; + + const allText = [ + currentReadings.primaLettura, + currentReadings.salmo, + currentReadings.secondaLettura, + currentReadings.vangelo + ].join(' ').toLowerCase(); + + const stopWords = new Set(['il', 'lo', 'la', 'i', 'gli', 'le', 'di', 'a', 'da', 'in', 'con', 'su', 'per', 'tra', 'fra', 'e', 'o', 'che', 'non', 'si', 'del', 'al', 'dal', 'nel', 'col', 'sul', 'mio', 'tuo', 'suo', 'noi', 'voi', 'loro', 'mio', 'mia', 'miei', 'mie']); + const words = allText.match(/\b(\w+)\b/g) || []; + const keywords = Array.from(new Set(words.filter(w => w.length > 3 && !stopWords.has(w)))); + this.keywords.set(keywords.slice(0, 20)); + + const songs = this.cantiService.canti(); + const scoredSongs = songs.map(song => { + let score = 0; + const titleLower = song.titolo.toLowerCase(); + const lyricsLower = (song.testo || '').toLowerCase(); + + keywords.forEach(kw => { + if (titleLower.includes(kw)) score += 10; + const regex = new RegExp(`\\b${kw}\\b`, 'g'); + const matches = lyricsLower.match(regex); + if (matches) score += matches.length; + }); + + return { song, score }; + }); + + const results = scoredSongs + .filter(s => s.score > 0) + .sort((a, b) => b.score - a.score); + + const moments = this.cantiService.indiceLiturgico(); + const grouped: {moment: string, canti: Canto[]}[] = []; + + moments.forEach(m => { + const cantiInMoment = results + .filter(s => s.song.id_momenti?.includes(m.id)) + .slice(0, 3) + .map(s => s.song); + + if (cantiInMoment.length > 0) { + grouped.push({ moment: m.tag_name, canti: cantiInMoment }); + } + }); + + this.groupedSuggestions.set(grouped); + this.suggestions.set(results.slice(0, 20).map(s => s.song)); + } +} diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 854aeac..6dff6f3 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -35,10 +35,13 @@ export class SettingsService { this.fullscreenMode.set(savedFullscreen === 'true'); } + /* const savedEditor = localStorage.getItem('show-editor'); if (savedEditor !== null) { this.showEditor.set(savedEditor === 'true'); } + */ + this.showEditor.set(false); // Funzione temporaneamente disabilitata const savedAutoAdvance = localStorage.getItem('auto-advance'); if (savedAutoAdvance !== null) { diff --git a/src/app/version.ts b/src/app/version.ts index 8f1ddd7..0619e24 100644 --- a/src/app/version.ts +++ b/src/app/version.ts @@ -1 +1 @@ -export const VERSION = '2026.05.16.1715'; +export const VERSION = '2026.05.16.1840';