diff --git a/deploy_ionic.sh b/deploy_ionic.sh deleted file mode 100755 index 99d38b6..0000000 --- a/deploy_ionic.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash - -# --- Caricamento variabili d'ambiente --- -if [ -f .env ]; then - export $(grep -v '^#' .env | xargs) -else - echo "❌ Errore: File .env non trovato." - exit 1 -fi - -# --- Configurazione Parallelismo --- -THREADS=${1:-${FTP_THREADS:-30}} - -# --- Configurazione FTP --- -FTP_HOST="ftp.canticristiani.it" -FTP_USER="canticristiani.it" -FTP_PASS="$FTP_PASSWORD" -REMOTE_DIR="${BASE_HREF%/}" # Rimuove lo slash finale se presente per la directory remota - -if [ -z "$FTP_PASS" ]; then - echo "❌ Errore: FTP_PASSWORD non definita nel file .env" - exit 1 -fi - -# --- Aggiornamento Versione --- -VERSION=$(date +'%Y.%m.%d.%H%M') -echo "export const VERSION = '$VERSION';" > src/app/version.ts -echo "🏷️ Versione aggiornata a: $VERSION" - -# --- Configurazione Email Parametrica --- -node -e " -const fs = require('fs'); -const envEmail = process.env.CONTACT_EMAIL || 'info@canticristiani.it'; -['src/environments/environment.ts', 'src/environments/environment.prod.ts'].forEach(file => { - if (fs.existsSync(file)) { - let content = fs.readFileSync(file, 'utf8'); - content = content.replace(/contactEmail:\s*'[^']*'/g, \`contactEmail: '\${envEmail}'\`); - fs.writeFileSync(file, content, 'utf8'); - console.log(\`📧 Aggiornata email di contatto in \${file} a: \${envEmail}\`); - } -}); -" - -# --- Build della PWA --- -echo "📦 1/2 Build della PWA (Production) in corso..." -# Usiamo la variabile d'ambiente BASE_HREF dal file .env -npm run build -- --configuration=production --base-href=$BASE_HREF || { echo "❌ Errore nella build"; exit 1; } - -if [ ! -d "www" ]; then - echo "❌ Errore: Cartella 'www' non trovata." - exit 1 -fi - -# --- Upload via FTP --- -echo "🚀 2/2 Caricamento parallelo ($THREADS connessioni) su $FTP_HOST$REMOTE_DIR..." -cd www - -TOTAL_FILES=$(find . -type f | wc -l | xargs) -PROGRESS_LOG=$(mktemp) -ERROR_LOG=$(mktemp) - -show_progress() { - local current=0 - while [ "$current" -lt "$TOTAL_FILES" ]; do - current=$(wc -l < "$PROGRESS_LOG" | xargs) - local percent=$((current * 100 / TOTAL_FILES)) - local bar_size=20 - local num_hash=$((percent * bar_size / 100)) - local bar=$(printf "%${num_hash}s" | tr ' ' '#' 2>/dev/null) - local spaces=$(printf "%$((bar_size - num_hash))s" | tr ' ' '-') - printf "\r[%-20s] %d%% (%d/%d) caricati..." "$bar$spaces" "$percent" "$current" "$TOTAL_FILES" - sleep 0.2 - done -} - -show_progress & -BAR_PID=$! - -# Lancio dei job paralleli -find . -type f | while read -r file; do - REMOTE_FILE_PATH=${file#./} - - # Eseguiamo curl e segniamo SEMPRE il progresso (anche se fallisce) - # Se fallisce, scriviamo il nome del file nell'error log - ( - # Usiamo --retry per gestire errori temporanei di connessione - if ! curl -s --retry 3 --retry-delay 1 --connect-timeout 10 -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$file" "ftp://$FTP_HOST$REMOTE_DIR/$REMOTE_FILE_PATH"; then - echo "$REMOTE_FILE_PATH" >> "$ERROR_LOG" - fi - echo 1 >> "$PROGRESS_LOG" - ) & - - # Gestione THREADS - while [ $(jobs -r | wc -l) -ge "$THREADS" ]; do - sleep 0.05 - done -done - -# Attendi fine caricamenti -wait - -# Stop barra -sleep 0.5 -kill $BAR_PID 2>/dev/null -echo "" - -# Controllo errori -ERRORS=$(wc -l < "$ERROR_LOG" | xargs) -if [ "$ERRORS" -gt 0 ]; then - echo "⚠️ Deploy completato con $ERRORS errori." - echo "I seguenti file non sono stati caricati:" - cat "$ERROR_LOG" -else - echo "✅ Deploy completato con successo senza errori!" -fi - -# Cleanup -rm "$PROGRESS_LOG" "$ERROR_LOG" -printf "L'app è disponibile su http://canticristiani.it${BASE_HREF}\n" diff --git a/deploy_localpwa.sh b/deploy_localpwa.sh index 9388280..3f6024b 100755 --- a/deploy_localpwa.sh +++ b/deploy_localpwa.sh @@ -17,6 +17,7 @@ VERSION=$(date +'%Y.%m.%d.%H%M') echo "export const VERSION = '$VERSION';" > src/app/version.ts echo "🏷️ Versione aggiornata a: $VERSION" + # 2. Build dell'applicazione echo "📦 Compilazione in corso (Production Build)..." if ! npm run build -- --configuration=production --base-href=/; then @@ -54,7 +55,7 @@ echo "--------------------------------------------------------" echo "📱 INSTALLAZIONE SU CELLULARE:" echo "1. Assicurati che il cellulare sia sulla stessa rete Wi-Fi." echo "2. Apri il browser (Chrome su Android, Safari su iOS)." -echo "3. Vai all'indirizzo: http://$IP_ADDRESS:8080/ionic/" +echo "3. Vai all'indirizzo: http://\$IP_ADDRESS:8080/" echo " ⚠️ NOTA: Per usare il MICROFONO su cellulare, i browser richiedono HTTPS." echo " Puoi usare uno strumento come 'ngrok' o 'local-ssl-proxy' per testarlo con HTTPS." echo "4. Una volta caricata l'app:" diff --git a/deploy_www.sh b/deploy_www.sh index b12c014..86be67b 100755 --- a/deploy_www.sh +++ b/deploy_www.sh @@ -41,6 +41,7 @@ const envEmail = process.env.CONTACT_EMAIL || 'info@canticristiani.it'; }); " + # --- Build della PWA (base-href=/ per installare nella root) --- echo "📦 1/2 Build della PWA (Production) per ROOT (www.canticristiani.it) in corso..." npm run build -- --configuration=production --base-href=/ || { echo "❌ Errore nella build"; exit 1; } diff --git a/ngsw-config.json b/ngsw-config.json index a9a7085..ae56e83 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -7,7 +7,6 @@ "installMode": "prefetch", "resources": { "files": [ - "/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..ba15edc --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,10 @@ + + RewriteEngine On + + # Se il file o la directory richiesti esistono, li serve direttamente + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + + # Altrimenti reindirizza tutto a index.html (gestito da Angular) + RewriteRule ^ index.html [L] + diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png index b4f8114..f9375e1 100644 Binary files a/public/icons/icon-128x128.png and b/public/icons/icon-128x128.png differ diff --git a/public/icons/icon-144x144.png b/public/icons/icon-144x144.png index b4f8114..37894af 100644 Binary files a/public/icons/icon-144x144.png and b/public/icons/icon-144x144.png differ diff --git a/public/icons/icon-152x152.png b/public/icons/icon-152x152.png index b4f8114..a856095 100644 Binary files a/public/icons/icon-152x152.png and b/public/icons/icon-152x152.png differ diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png index b4f8114..750473c 100644 Binary files a/public/icons/icon-192x192.png and b/public/icons/icon-192x192.png differ diff --git a/public/icons/icon-384x384.png b/public/icons/icon-384x384.png index b4f8114..d05a3d5 100644 Binary files a/public/icons/icon-384x384.png and b/public/icons/icon-384x384.png differ diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png index b4f8114..f5a59bc 100644 Binary files a/public/icons/icon-512x512.png and b/public/icons/icon-512x512.png differ diff --git a/public/icons/icon-72x72.png b/public/icons/icon-72x72.png index b4f8114..61c6986 100644 Binary files a/public/icons/icon-72x72.png and b/public/icons/icon-72x72.png differ diff --git a/public/icons/icon-96x96.png b/public/icons/icon-96x96.png index b4f8114..81dea00 100644 Binary files a/public/icons/icon-96x96.png and b/public/icons/icon-96x96.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 45d2933..45b3d6b 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -2,56 +2,70 @@ "name": "CantiCristiani", "short_name": "CantiCristiani", "display": "standalone", - "scope": "./", - "start_url": "./", + "scope": "/", + "start_url": "/", + "theme_color": "#3880ff", + "background_color": "#ffffff", "icons": [ { - "src": "icons/icon-72x72.png", + "src": "assets/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-96x96.png", + "src": "assets/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-128x128.png", + "src": "assets/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-144x144.png", + "src": "assets/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-152x152.png", + "src": "assets/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-192x192.png", + "src": "assets/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-384x384.png", + "src": "assets/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" }, { - "src": "icons/icon-512x512.png", + "src": "assets/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", - "purpose": "maskable any" + "purpose": "any" + }, + { + "src": "assets/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "assets/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 64f8d7e..b639a4d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,21 +19,31 @@ export class AppComponent { private setupUpdates() { if (this.swUpdate.isEnabled) { - // 1. Check for updates immediately as soon as the application stabilizes - const appIsStable$ = this.appRef.isStable.pipe( - first(isStable => isStable === true) - ); + // Wait for the application to stabilize before running update checks or starting intervals + this.appRef.isStable.pipe( + filter(stable => stable), + first() + ).subscribe(() => { + console.log('[PWA-Update] App is stable. Initializing update checks...'); + + // 1. Check for updates immediately + this.swUpdate.checkForUpdate().catch(err => { + console.warn('[PWA-Update] Failed immediate startup update check:', err); + }); - appIsStable$.subscribe(async () => { - console.log('[PWA-Update] App is stable, checking for PWA updates immediately...'); - try { - await this.swUpdate.checkForUpdate(); - } catch (err) { - console.warn('[PWA-Update] Failed startup update check:', err); - } + // 2. Periodic check in background every 30 seconds + const every30Seconds$ = interval(30 * 1000); + every30Seconds$.subscribe(async () => { + console.log('[PWA-Update] Periodic check for updates (every 30s)...'); + try { + await this.swUpdate.checkForUpdate(); + } catch (err) { + console.warn('[PWA-Update] Failed periodic update check:', err); + } + }); }); - // 2. Check for updates when the app is resumed/focused + // 3. Check for updates when the app is resumed/focused fromEvent(document, 'visibilitychange') .pipe(filter(() => document.visibilityState === 'visible')) .subscribe(async () => { @@ -45,17 +55,6 @@ export class AppComponent { } }); - // 3. Periodic check in background every 5 minutes - const everyFiveMinutes$ = interval(5 * 60 * 1000); - everyFiveMinutes$.subscribe(async () => { - console.log('[PWA-Update] Periodic check for updates...'); - try { - await this.swUpdate.checkForUpdate(); - } catch (err) { - console.warn('[PWA-Update] Failed periodic update check:', err); - } - }); - // 4. Activate update and reload when a new version is ready this.swUpdate.versionUpdates .pipe(filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY')) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5d8062b..8c8b9d9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -20,8 +20,7 @@ import { ServiceWorkerModule } from '@angular/service-worker'; IonicStorageModule.forRoot(), ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), - // Register the ServiceWorker as soon as the application is stable - // or after 30 seconds (whichever comes first). + // Register the ServiceWorker as soon as possible registrationStrategy: 'registerImmediately' }) ], diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 436264b..ced4088 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -4,7 +4,7 @@
- CantiCristiani + {{ appName }} v{{ version }}
@@ -192,6 +192,29 @@ (click)="onInteraction()">
+ +
+ +
+
@@ -394,3 +417,21 @@
+ + +
+
+
+ Installa su iPhone + + + +
+
+

+ Tocca il pulsante Condividi in basso e seleziona "Aggiungi a schermata Home". +

+
+
+
+
diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index 13b6dd5..dc70074 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -953,3 +953,323 @@ ion-title { color: #c0392b !important; border-color: #c0392b !important; } + +/* ========================================================================== + PWA INSTALLATION PROMOTIONS (Android & iOS) + ========================================================================== */ + +/* 1. Android Installation Banner */ +.pwa-android-banner { + --background: rgba(255, 255, 255, 0.04); + background: rgba(255, 255, 255, 0.04); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-left: 4px solid var(--ion-color-secondary); + border-radius: 16px; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + animation: slideInBanner 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + + &:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.12); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); + } + + .banner-inner { + display: flex; + align-items: center; + padding: 14px 16px; + gap: 12px; + } + + .banner-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + background: rgba(var(--ion-color-secondary-rgb), 0.12); + border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.2); + width: 42px; + height: 42px; + border-radius: 12px; + flex-shrink: 0; + + .banner-icon { + font-size: 1.5rem; + color: var(--ion-color-secondary); + } + } + + .banner-text-wrapper { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + overflow: hidden; + + .banner-title { + font-size: 0.95rem; + font-weight: 700; + color: white; + margin: 0; + letter-spacing: 0.2px; + } + + .banner-subtitle { + font-size: 0.78rem; + line-height: 1.35; + color: rgba(255, 255, 255, 0.65); + margin: 0; + font-weight: 400; + } + } + + .banner-actions { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + + .install-action-btn { + --border-radius: 10px; + --box-shadow: 0 4px 10px rgba(var(--ion-color-secondary-rgb), 0.2); + font-weight: 700; + font-size: 0.8rem; + height: 32px; + margin: 0; + text-transform: none; + letter-spacing: 0.2px; + transition: transform 0.2s ease; + + &:active { + transform: scale(0.95); + } + } + + .dismiss-action-btn { + --color: rgba(255, 255, 255, 0.5); + --padding-start: 4px; + --padding-end: 4px; + margin: 0; + height: 32px; + width: 32px; + + ion-icon { + font-size: 1.3rem; + } + + &:active { + opacity: 0.7; + } + } + } +} + +/* 2. iOS Safari Contextual Tooltip */ +.pwa-ios-tooltip-wrapper { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + width: 90%; + max-width: 385px; + pointer-events: none; + filter: drop-shadow(0 12px 36px rgba(0, 0, 0, 0.45)); + transition: bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &.has-player { + bottom: 84px; /* Float above active bottom player footer */ + } + + .pwa-ios-tooltip { + pointer-events: auto; + display: flex; + flex-direction: column; + background: rgba(26, 26, 46, 0.96) !important; + backdrop-filter: blur(25px); + -webkit-backdrop-filter: blur(25px); + border: 1.5px solid rgba(var(--ion-color-secondary-rgb), 0.25); + border-radius: 20px; + padding: 14px 16px; + position: relative; + box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.02); + animation: floatTooltip 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.275), bounceGently 3s ease-in-out infinite; + + .tooltip-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + + .tooltip-title { + font-size: 0.95rem; + font-weight: 700; + color: var(--ion-color-secondary); + letter-spacing: 0.3px; + } + + .dismiss-btn { + --color: rgba(255, 255, 255, 0.45); + --padding-start: 4px; + --padding-end: 4px; + margin: 0; + height: 24px; + width: 24px; + + ion-icon { + font-size: 1.2rem; + } + + &:active { + opacity: 0.7; + } + } + } + + .tooltip-body { + .tooltip-text { + font-size: 0.82rem; + line-height: 1.45; + color: rgba(255, 255, 255, 0.85); + margin: 0; + font-weight: 400; + + strong { + color: white; + font-weight: 600; + } + + .safari-icon-inline { + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + padding: 3px; + border-radius: 6px; + vertical-align: middle; + margin: 0 3px; + width: 20px; + height: 20px; + + ion-icon { + font-size: 0.95rem; + color: #007aff; /* Apple System Blue */ + } + } + } + } + + .tooltip-arrow { + width: 0; + height: 0; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + border-top: 12px solid rgba(26, 26, 46, 0.96); + position: absolute; + bottom: -11px; + left: 50%; + transform: translateX(-50%); + + &::after { + content: ''; + width: 0; + height: 0; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + border-top: 12px solid rgba(var(--ion-color-secondary-rgb), 0.25); + position: absolute; + bottom: -1px; + left: -12px; + z-index: -1; + } + } + } +} + +/* Animations */ +@keyframes slideInBanner { + from { + opacity: 0; + transform: translateY(-16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes floatTooltip { + from { + opacity: 0; + transform: translate(-50%, 20px) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, 0) scale(1); + } +} + +@keyframes bounceGently { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +/* High Contrast mode adaptations */ +:host-context(body.high-contrast) { + .pwa-android-banner { + background: #ffffff !important; + border: 2px solid #000000 !important; + border-left-width: 6px !important; + + .banner-title, .banner-subtitle { + color: #000000 !important; + } + .banner-icon { + color: #000000 !important; + } + .banner-icon-wrapper { + background: rgba(0, 0, 0, 0.05) !important; + border-color: #000000 !important; + } + .install-action-btn { + --background: #000000 !important; + --color: #ffffff !important; + } + .dismiss-action-btn { + --color: #000000 !important; + } + } + + .pwa-ios-tooltip { + background: #ffffff !important; + border: 2px solid #000000 !important; + + .tooltip-title, .tooltip-text, .tooltip-text strong { + color: #000000 !important; + } + .dismiss-btn { + --color: #000000 !important; + } + .tooltip-arrow { + border-top-color: #ffffff !important; + &::after { + border-top-color: #000000 !important; + } + } + .safari-icon-inline { + background: rgba(0, 0, 0, 0.05) !important; + border-color: #000000 !important; + ion-icon { + color: #000000 !important; + } + } + } +} diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index b612ef4..adfdd7a 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -15,6 +15,7 @@ import { MyCantiService } from '../services/my-canti.service'; import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component'; import { CantiLettureService } from '../services/canti-letture.service'; import { ComunitaService } from '../services/comunita.service'; +import { environment } from '../../environments/environment'; @Component({ selector: 'app-home', @@ -33,10 +34,14 @@ export class HomePage implements OnDestroy { public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null); public loadedThumbs = new Set(); public version = VERSION; + public appName = environment.appName; public isAddingSongs = signal(false); public reorderList = signal([]); public limit = signal(30); + public showAndroidBanner = signal(false); + public showIosTooltip = signal(false); + public fontSize = signal(1.0); public youtubePlayerService = inject(YoutubePlayerService); public comunitaService = inject(ComunitaService); @@ -110,23 +115,28 @@ export class HomePage implements OnDestroy { const onlyMine = this.showOnlyMine(); const topTen = this.showTopTen(); const suggeriti = this.showSuggeriti(); - - let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()]; + const activeIds = this.playlistService.activeListIds(); + const selectionMode = this.playlistService.selectionMode(); - // Filter by Community if code is set and filter is active + // 1. Get base list depending on community filter + let list: any[] = []; const comunitaCode = this.comunitaService.comunitaCode(); const comunitaIds = this.comunitaService.comunitaCantiIds(); + if (comunitaCode && this.comunitaService.isFilterActive()) { - // Include parish custom/unvalidated canti - list = [...list, ...this.comunitaService.comunitaCantiPersonali()]; - + // Base is standard canti + my canti + community custom canti + list = [ + ...this.cantiService.canti(), + ...this.myCantiService.myCanti(), + ...this.comunitaService.comunitaCantiPersonali() + ]; + + // Filter to only community canti list = list.filter(c => { return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id); }); - // Deduplicate: if a song exists in both validated and unvalidated lists, - // the NON-VALIDATED (parish-customized) version always takes precedence, - // so parishes can always personalize their songs even if never validated. + // Deduplicate: parish-customized versions take precedence const seen = new Map(); for (const canto of list) { const key = canto.id_canti || canto.id; @@ -134,16 +144,14 @@ export class HomePage implements OnDestroy { if (!existing) { seen.set(key, canto); } else { - // Precedence to non-validated (parish custom) version: - // overwrite only if current entry is validated and new one is non-validated if (!existing.nonValidato && canto.nonValidato) { seen.set(key, canto); } } } list = Array.from(seen.values()); - - // Sort list by community song number (num_canto) ASC! + + // Sort by community song number (num_canto) const cantiInfo = this.comunitaService.comunitaCantiInfo(); list.sort((a, b) => { const infoA = cantiInfo.find(x => x.id_canti === a.id_canti || x.id_canti === Number(a.id)); @@ -152,49 +160,77 @@ export class HomePage implements OnDestroy { const numB = infoB ? Number(infoB.num_canto) : 999999; return numA - numB; }); + } else { + // General context: only standard canti + my canti + list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()]; } - if (onlyMine) { - list = this.myCantiService.myCanti(); + // 2. Cumulative filtering + + // Filter by selectionMode / reorderList if in selection mode and not adding songs + if (selectionMode && !this.isAddingSongs()) { + return this.reorderList(); } - + + // A. Filter by Playlist (if activeIds is present) + if (activeIds.length > 0 && !selectionMode) { + list = activeIds + .map(id => list.find(c => c.id === id)) + .filter((c): c is any => !!c); + } + + // B. Filter by "Only Mine" (Miei) + if (onlyMine) { + const myIds = new Set(this.myCantiService.myCanti().map(c => c.id)); + list = list.filter(c => myIds.has(c.id)); + } + + // C. Filter by Liturgical Moment if (litId !== null) { list = list.filter(c => c.id_momenti?.includes(litId)); } + + // D. Filter by Thematic Moment if (temId !== null) { list = list.filter(c => c.id_momenti?.includes(temId)); } + // E. Filter by Suggested (Suggeriti) if (suggeriti) { const suggMap = this.cantiLettureService.suggestionsMap(); list = list.filter(c => suggMap.has(c.id_canti)); } - // Special List handling (from QR Code or Saved Playlists) - const activeIds = this.playlistService.activeListIds(); - const selectionMode = this.playlistService.selectionMode(); - - // If we are in selection mode, only show restricted list if NOT adding songs - if (selectionMode && !this.isAddingSongs()) { - return this.reorderList(); + // F. Filter by Search Query + if (query) { + const processedQuery = query.replace(/\buno\b/g, '1'); + const numberMatchPattern = processedQuery.match(/^(?:numero|nr\.?)\s*(\d+)$/); + + if (numberMatchPattern) { + const targetId = numberMatchPattern[1]; + list = list.filter(c => { + const commNum = this.getCommunitySongNumber(c); + return commNum ? commNum === targetId : c.id_canti.toString() === targetId; + }); + } else { + list = list.filter(c => { + const titleMatch = this.normalize(c.titolo).includes(query); + const authorMatch = this.normalize(c.autore).includes(query); + const commNum = this.getCommunitySongNumber(c); + const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query); + + const lyricsPlain = (c.testo || '') + .replace(/{.*?}/g, '') + .replace(/\n/g, ' '); + const lyricsMatch = this.normalize(lyricsPlain).includes(query); + + return titleMatch || authorMatch || lyricsMatch || numberMatch; + }); + } } - // If we have an active playlist and NOT in selection mode, show ONLY those songs - if (activeIds.length > 0 && !selectionMode) { - // Filter only songs in the special list and KEEP THE ORDER - return activeIds - .map(id => list.find(c => c.id === id)) - .filter((c): c is any => !!c); - } - - if (suggeriti) { - const suggMap = this.cantiLettureService.suggestionsMap(); - list = list.sort((a, b) => { - const pesoA = suggMap.get(a.id_canti) || 0; - const pesoB = suggMap.get(b.id_canti) || 0; - return pesoB - pesoA; - }); - } else if (topTen) { + // G. Sorting/Ordering + if (topTen) { const eseguiti = this.cantiService.cantiEseguiti(); const eseguitiMap = new Map(); eseguiti.forEach(x => eseguitiMap.set(x.id_canti, x.num)); @@ -204,37 +240,16 @@ export class HomePage implements OnDestroy { const numB = eseguitiMap.get(b.id_canti) || 0; return numB - numA; }); - } - - if (!query) return list; - - // Converte "uno" in "1" per facilitare la ricerca vocale (es. "numero uno") - const processedQuery = query.replace(/\buno\b/g, '1'); - - // Se la ricerca inizia con "numero" o "nr", filtra esattamente per id_canti/comunita number - const numberMatchPattern = processedQuery.match(/^(?:numero|nr\.?)\s*(\d+)$/); - if (numberMatchPattern) { - const targetId = numberMatchPattern[1]; - return list.filter(c => { - const commNum = this.getCommunitySongNumber(c); - return commNum ? commNum === targetId : c.id_canti.toString() === targetId; + } else if (suggeriti) { + const suggMap = this.cantiLettureService.suggestionsMap(); + list = list.sort((a, b) => { + const pesoA = suggMap.get(a.id_canti) || 0; + const pesoB = suggMap.get(b.id_canti) || 0; + return pesoB - pesoA; }); } - return list.filter(c => { - const titleMatch = this.normalize(c.titolo).includes(query); - const authorMatch = this.normalize(c.autore).includes(query); - const commNum = this.getCommunitySongNumber(c); - const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query); - - // Strip tags like {Chorus} and newlines from lyrics before searching - const lyricsPlain = (c.testo || '') - .replace(/{.*?}/g, '') - .replace(/\n/g, ' '); - const lyricsMatch = this.normalize(lyricsPlain).includes(query); - - return titleMatch || authorMatch || lyricsMatch || numberMatch; - }); + return list; }); public visibleCanti = computed(() => { @@ -242,6 +257,49 @@ export class HomePage implements OnDestroy { }); constructor() { + // Check if install prompts should be visible + const androidDismissed = localStorage.getItem('pwa-android-dismissed') === 'true'; + const iosDismissed = localStorage.getItem('pwa-ios-dismissed') === 'true'; + + this.showAndroidBanner.set( + this.settingsService.isAndroid() && + !this.settingsService.isStandalone() && + !androidDismissed + ); + + this.showIosTooltip.set( + this.settingsService.isIos() && + !this.settingsService.isStandalone() && + !iosDismissed + ); + + // Track initial community filter state to avoid clearing during the initial run of the effect + let prevFilterActive = this.comunitaService.isFilterActive(); + let prevComunitaCode = this.comunitaService.comunitaCode(); + + // Automatically reset all other filters and active playlist when toggling the community filter or changing community + effect(() => { + const active = this.comunitaService.isFilterActive(); + const code = this.comunitaService.comunitaCode(); + + if (active !== prevFilterActive || code !== prevComunitaCode) { + prevFilterActive = active; + prevComunitaCode = code; + + this.selectedLiturgico.set(null); + this.selectedTematico.set(null); + this.showOnlyMine.set(false); + this.showTopTen.set(false); + this.showSuggeriti.set(false); + this.playlistService.activeListIds.set([]); + this.playlistService.activeListName.set(null); + this.playlistService.activePlaylistId.set(null); + this.searchQuery.set(''); + this.activeFilterType.set(null); + this.limit.set(10); + } + }, { allowSignalWrites: true }); + // Sync speech recognition results to search query effect(() => { const transcript = this.audioEngine.searchTranscript(); @@ -307,6 +365,34 @@ export class HomePage implements OnDestroy { }, { allowSignalWrites: true }); } + dismissAndroidBanner(event?: Event) { + if (event) event.stopPropagation(); + this.showAndroidBanner.set(false); + localStorage.setItem('pwa-android-dismissed', 'true'); + } + + dismissIosTooltip(event?: Event) { + if (event) event.stopPropagation(); + this.showIosTooltip.set(false); + localStorage.setItem('pwa-ios-dismissed', 'true'); + } + + async installAndroidPwa() { + if (this.settingsService.deferredPrompt()) { + await this.settingsService.installPwa(); + this.dismissAndroidBanner(); + } else { + const toast = await this.toastCtrl.create({ + message: 'Tocca il menu del browser (i tre puntini in alto a destra) e seleziona "Aggiungi a schermata Home" o "Installa app".', + duration: 6000, + position: 'bottom', + color: 'secondary', + buttons: [{ text: 'OK', role: 'cancel' }] + }); + await toast.present(); + } + } + handleImport(base64: string) { try { const decoded = decodeURIComponent(escape(atob(base64))); @@ -382,10 +468,6 @@ export class HomePage implements OnDestroy { toggleOnlyMine() { this.showOnlyMine.update(v => !v); - if (this.showOnlyMine()) { - this.selectedLiturgico.set(null); - this.selectedTematico.set(null); - } this.limit.set(10); } @@ -401,7 +483,6 @@ export class HomePage implements OnDestroy { } else if (type === 'tematico') { this.selectedTematico.update(cur => cur === id ? null : id); } - this.showOnlyMine.set(false); this.activeFilterType.set(null); this.limit.set(10); } @@ -510,10 +591,6 @@ export class HomePage implements OnDestroy { this.playlistService.activePlaylistId.set(pl.id); this.activeFilterType.set(null); this.limit.set(50); - // Clear other filters - this.selectedLiturgico.set(null); - this.selectedTematico.set(null); - this.showOnlyMine.set(false); } toggleTopTen() { @@ -580,7 +657,7 @@ export class HomePage implements OnDestroy { const numId = parseInt(id, 10); if (isNaN(numId)) return null; const es = this.cantiService.cantiEseguiti().find(x => x.id_canti === numId); - return es ? es.num : null; + return es ? es.num : 0; } onImgLoad(id: string) { diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index a373b7d..65689bf 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -16,7 +16,7 @@

Installa Applicazione

-

Aggiungi CantiCristiani alla schermata home

+

Aggiungi {{ appName }} alla schermata home

Installa diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts index 44d550e..45a560e 100644 --- a/src/app/pages/settings/settings.page.ts +++ b/src/app/pages/settings/settings.page.ts @@ -37,6 +37,7 @@ export class SettingsPage { public version = VERSION; public contactEmail = environment.contactEmail; + public appName = environment.appName; public showIosInstructions = false; constructor() {} diff --git a/src/app/services/canti-letture.service.ts b/src/app/services/canti-letture.service.ts index 2e90d25..b65cd00 100644 --- a/src/app/services/canti-letture.service.ts +++ b/src/app/services/canti-letture.service.ts @@ -4,8 +4,10 @@ import { Storage } from '@ionic/storage-angular'; import { firstValueFrom } from 'rxjs'; export interface Suggestion { - id_canto: number; - peso: number; + id_canto?: number; + id_canti?: number; + peso?: number; + score?: number; } export interface MassDetails { @@ -62,9 +64,13 @@ export class CantiLettureService { if (Array.isArray(mass.suggestions)) { // Legacy flat suggestions format mass.suggestions.forEach(s => { - if (s && s.id_canto) { - newMap.set(s.id_canto, s.peso); - newMomentsMap.set(s.id_canto, ['Generale']); + if (s) { + const id = s.id_canto || (s as any).id_canti; + const weight = s.peso !== undefined ? s.peso : (s as any).score; + if (id) { + newMap.set(id, weight); + newMomentsMap.set(id, ['Generale']); + } } }); } else { @@ -73,17 +79,21 @@ export class CantiLettureService { const momentSuggestions = (mass.suggestions as any)[moment]; if (Array.isArray(momentSuggestions)) { momentSuggestions.forEach(s => { - if (s && s.id_canto) { - // Save max weight - const existingWeight = newMap.get(s.id_canto) || 0; - if (s.peso > existingWeight) { - newMap.set(s.id_canto, s.peso); - } + if (s) { + const id = s.id_canto || (s as any).id_canti; + const weight = s.peso !== undefined ? s.peso : (s as any).score; + if (id) { + // Save max weight + const existingWeight = newMap.get(id) || 0; + if (weight > existingWeight) { + newMap.set(id, weight); + } - // Save liturgical moment - const existingMoments = newMomentsMap.get(s.id_canto) || []; - if (!existingMoments.includes(moment)) { - newMomentsMap.set(s.id_canto, [...existingMoments, moment]); + // Save liturgical moment + const existingMoments = newMomentsMap.get(id) || []; + if (!existingMoments.includes(moment)) { + newMomentsMap.set(id, [...existingMoments, moment]); + } } } }); @@ -129,8 +139,12 @@ export class CantiLettureService { let fetchedData: CantiLettureData | null = null; const isProduction = window.location.hostname.includes('canticristiani.it'); - const primaryUrl = isProduction ? `${window.location.origin}${this.SECURE_JSON_URL}` : this.JSON_URL; - const fallbackUrl = isProduction ? this.JSON_URL : `${window.location.origin}${this.SECURE_JSON_URL}`; + const primaryUrl = isProduction + ? `${window.location.origin}${this.SECURE_JSON_URL}` + : 'https://www.canticristiani.it/api/cantiletture.json'; + const fallbackUrl = isProduction + ? 'https://www.canticristiani.it/api/cantiletture.json' + : this.JSON_URL; try { console.log('Fetching mass data from primary URL:', primaryUrl); @@ -182,7 +196,11 @@ export class CantiLettureService { // Always select today's mass if available on load, else find closest future date if (massesList.length > 0) { - const todayStr = new Date().toISOString().split('T')[0]; + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const todayStr = `${year}-${month}-${day}`; const match = massesList.find(m => m.date === todayStr); if (match) { this.selectedMassDate.set(match.date); diff --git a/src/app/services/my-canti.spec.ts b/src/app/services/my-canti.spec.ts index fef0939..1c790a7 100644 --- a/src/app/services/my-canti.spec.ts +++ b/src/app/services/my-canti.spec.ts @@ -1,13 +1,37 @@ import { TestBed } from '@angular/core/testing'; +import { MyCantiService } from './my-canti.service'; +import { Storage } from '@ionic/storage-angular'; +import { CantiService } from './canti.service'; +import { ToastController } from '@ionic/angular'; -import { MyCanti } from './my-canti'; - -describe('MyCanti', () => { - let service: MyCanti; +describe('MyCantiService', () => { + let service: MyCantiService; + let mockStorage: any; + let mockCantiService: any; + let mockToastCtrl: any; beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(MyCanti); + mockStorage = jasmine.createSpyObj('Storage', ['create', 'get', 'set']); + mockStorage.get.and.returnValue(Promise.resolve([])); + mockStorage.set.and.returnValue(Promise.resolve()); + + mockCantiService = jasmine.createSpyObj('CantiService', ['getStorage']); + mockCantiService.getStorage.and.returnValue(mockStorage); + + mockToastCtrl = jasmine.createSpyObj('ToastController', ['create']); + mockToastCtrl.create.and.returnValue(Promise.resolve({ + present: jasmine.createSpy('present').and.returnValue(Promise.resolve()) + })); + + TestBed.configureTestingModule({ + providers: [ + MyCantiService, + { provide: Storage, useValue: mockStorage }, + { provide: CantiService, useValue: mockCantiService }, + { provide: ToastController, useValue: mockToastCtrl } + ] + }); + service = TestBed.inject(MyCantiService); }); it('should be created', () => { diff --git a/src/app/services/playlist.service.spec.ts b/src/app/services/playlist.service.spec.ts new file mode 100644 index 0000000..7059658 --- /dev/null +++ b/src/app/services/playlist.service.spec.ts @@ -0,0 +1,74 @@ +import { TestBed } from '@angular/core/testing'; +import { PlaylistService } from './playlist.service'; +import { Storage } from '@ionic/storage-angular'; +import { CantiService } from './canti.service'; +import { ComunitaService } from './comunita.service'; +import { signal } from '@angular/core'; + +describe('PlaylistService', () => { + let service: PlaylistService; + let mockStorage: any; + let mockCantiService: any; + let mockComunitaService: any; + + beforeEach(async () => { + mockStorage = jasmine.createSpyObj('Storage', ['create', 'get', 'set']); + mockStorage.create.and.returnValue(Promise.resolve(mockStorage)); + mockStorage.get.and.callFake((key: string) => { + if (key === 'playlists') { + return Promise.resolve([{ id: '1', name: 'Playlist Generale', ids: ['101', '102'] }]); + } + if (key === 'playlists_comunita_123456') { + return Promise.resolve([{ id: '2', name: 'Playlist Comunita 123', ids: ['103'] }]); + } + return Promise.resolve(null); + }); + mockStorage.set.and.returnValue(Promise.resolve()); + + mockCantiService = jasmine.createSpyObj('CantiService', ['canti']); + mockCantiService.canti.and.returnValue([]); + + mockComunitaService = { + comunitaCode: signal(''), + isFilterActive: signal(false), + comunitaScalette: signal([]), + }; + + TestBed.configureTestingModule({ + providers: [ + PlaylistService, + { provide: Storage, useValue: mockStorage }, + { provide: CantiService, useValue: mockCantiService }, + { provide: ComunitaService, useValue: mockComunitaService } + ] + }); + + service = TestBed.inject(PlaylistService); + // Simulate init() completion + await service.init(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should load general playlists when community is not active', async () => { + mockComunitaService.isFilterActive.set(false); + mockComunitaService.comunitaCode.set(''); + + await service.loadPlaylistsForCurrentContext(); + + expect(service.playlists().length).toBe(1); + expect(service.playlists()[0].name).toBe('Playlist Generale'); + }); + + it('should load community-specific playlists when community is active', async () => { + mockComunitaService.comunitaCode.set('123456'); + mockComunitaService.isFilterActive.set(true); + + await service.loadPlaylistsForCurrentContext(); + + expect(service.playlists().length).toBe(1); + expect(service.playlists()[0].name).toBe('Playlist Comunita 123'); + }); +}); diff --git a/src/app/services/playlist.service.ts b/src/app/services/playlist.service.ts index fe46ea5..42729d4 100644 --- a/src/app/services/playlist.service.ts +++ b/src/app/services/playlist.service.ts @@ -1,4 +1,4 @@ -import { Injectable, signal, inject, computed } from '@angular/core'; +import { Injectable, signal, inject, computed, effect } from '@angular/core'; import { Storage } from '@ionic/storage-angular'; import { Canto, CantiService } from './canti.service'; import { ComunitaService } from './comunita.service'; @@ -50,19 +50,42 @@ export class PlaylistService { constructor() { this.init(); + + // Watch for context changes to dynamically reload the correct playlists + effect(() => { + // Trigger effect on changes to these signals + this.comunitaService.comunitaCode(); + this.comunitaService.isFilterActive(); + + // Load the playlists + this.loadPlaylistsForCurrentContext(); + }, { allowSignalWrites: true }); + } + + private getPlaylistsStorageKey(): string { + const code = this.comunitaService.comunitaCode(); + const isCommunityActive = this.comunitaService.isFilterActive(); + if (code && isCommunityActive) { + return `playlists_comunita_${code}`; + } + return 'playlists'; } async init() { const storage = await this.storage.create(); this._storage = storage; - const saved = await this._storage.get('playlists'); - if (saved) { - this.playlists.set(saved); - } - const last = await this._storage?.get('lastPlaylist'); - if (last) { - this.lastPlaylist.set(last); - } + await this.loadPlaylistsForCurrentContext(); + } + + async loadPlaylistsForCurrentContext() { + if (!this._storage) return; + const key = this.getPlaylistsStorageKey(); + const saved = await this._storage.get(key); + this.playlists.set(saved || []); + + const lastKey = `lastPlaylist_${key}`; + const last = await this._storage.get(lastKey); + this.lastPlaylist.set(last || null); } toggleSelectionMode() { @@ -92,6 +115,8 @@ export class PlaylistService { } async savePlaylist(name: string, ids: string[]) { + const key = this.getPlaylistsStorageKey(); + const lastKey = `lastPlaylist_${key}`; const editId = this.activePlaylistId(); let newPlaylist: any; @@ -116,8 +141,8 @@ export class PlaylistService { this.lastPlaylist.set(newPlaylist); this.activeListIds.set(ids); this.activeListName.set(name); - await this._storage?.set('playlists', this.playlists()); - await this._storage?.set('lastPlaylist', newPlaylist); + await this._storage?.set(key, this.playlists()); + await this._storage?.set(lastKey, newPlaylist); // Reset selection mode, IDs and editing state after saving this.selectedIds.set(new Set()); @@ -126,8 +151,9 @@ export class PlaylistService { } async deletePlaylist(id: string) { + const key = this.getPlaylistsStorageKey(); this.playlists.update(p => p.filter(pl => pl.id !== id)); - await this._storage?.set('playlists', this.playlists()); + await this._storage?.set(key, this.playlists()); } async generateQR(ids: string[], name: string): Promise { @@ -148,7 +174,7 @@ export class PlaylistService { const base64 = btoa(unescape(encodeURIComponent(data))); // Always use the production URL for sharing links as requested - const productionUrl = 'https://www.canticristiani.it/ionic'; + const productionUrl = 'https://www.canticristiani.it'; return `${productionUrl}/?import=${base64}`; } diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 81ebff0..15d19c6 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -5,14 +5,14 @@ import { Injectable, signal, effect } from '@angular/core'; }) export class SettingsService { /** Default mode for viewing songs: true = chords, false = text only */ - public showChordsDefault = signal(false); + public showChordsDefault = signal(true); /** UI Fullscreen mode (lyrics only): true = active */ public fullscreenMode = signal(false); /** Browser Fullscreen state: true = fullscreen active */ public browserFullscreen = signal( - !!( + typeof document !== 'undefined' && !!( document.fullscreenElement || (document as any).webkitFullscreenElement || (document as any).mozFullScreenElement || @@ -27,25 +27,25 @@ export class SettingsService { public showEditor = signal(false); /** Schermo sempre acceso: true = attiva Screen Wake Lock */ - public keepScreenOn = signal(false); + public keepScreenOn = signal(true); /** Funzionalità Comunità: true = il chip comunità è visibile nella home */ - public comunitaEnabled = signal(true); + public comunitaEnabled = signal(false); /** Invio dati statistici: true = invia pacchetto dati statistici */ public invioDatiStatistici = signal(false); /** Visualizza tag sotto autore nella lista canti: true = attivo */ - public showTagsInList = signal(false); + public showTagsInList = signal(true); /** Visualizza data update sotto autore nella lista canti: true = attivo */ - public showUpdateDate = signal(false); + public showUpdateDate = signal(true); /** Attiva autoscroll standard nel dettaglio canto: true = attivo */ - public enableStandardAutoscroll = signal(false); + public enableStandardAutoscroll = signal(true); /** Attiva autoscroll acustico nel dettaglio canto: true = attivo */ - public enableAcousticAutoscroll = signal(true); + public enableAcousticAutoscroll = signal(false); private wakeLock: any = null; @@ -54,6 +54,7 @@ export class SettingsService { public showInstallButton = signal(false); public isStandalone = signal(false); public isIos = signal(false); + public isAndroid = signal(false); constructor() { // Detect PWA status @@ -65,6 +66,9 @@ export class SettingsService { this.isIos.set( /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream ); + this.isAndroid.set( + /Android/i.test(navigator.userAgent) + ); } catch (e) { console.warn('Browser environment does not support display-mode media query'); } @@ -84,59 +88,103 @@ export class SettingsService { this.isStandalone.set(true); console.log('PWA was installed'); }); + + // Migration: force settings defaults once for existing users to match the new rules + const migrationKey = 'defaults-migrated-20260521'; + if (localStorage.getItem(migrationKey) !== 'true') { + localStorage.setItem('show-chords-default', 'true'); + localStorage.setItem('fullscreen-mode', this.isIos().toString()); + localStorage.setItem('show-editor', 'false'); + localStorage.setItem('auto-advance', 'true'); + localStorage.setItem('keep-screen-on', 'true'); + localStorage.setItem('comunita-enabled', 'false'); + localStorage.setItem('invio-dati-statistici', 'false'); + localStorage.setItem('show-tags-in-list', 'true'); + localStorage.setItem('show-update-date', 'true'); + localStorage.setItem('enable-standard-autoscroll', 'true'); + localStorage.setItem('enable-acoustic-autoscroll', 'false'); + + // ThemeService high contrast default + localStorage.setItem('high-contrast', 'true'); + + localStorage.setItem(migrationKey, 'true'); + } + const savedChords = localStorage.getItem('show-chords-default'); if (savedChords !== null) { this.showChordsDefault.set(savedChords === 'true'); + } else { + this.showChordsDefault.set(true); } const savedFullscreen = localStorage.getItem('fullscreen-mode'); if (savedFullscreen !== null) { this.fullscreenMode.set(savedFullscreen === 'true'); + } else { + this.fullscreenMode.set(this.isIos()); } const savedEditor = localStorage.getItem('show-editor'); if (savedEditor !== null) { this.showEditor.set(savedEditor === 'true'); + } else { + this.showEditor.set(false); } const savedAutoAdvance = localStorage.getItem('auto-advance'); if (savedAutoAdvance !== null) { this.autoAdvance.set(savedAutoAdvance === 'true'); + } else { + this.autoAdvance.set(true); } const savedKeepScreenOn = localStorage.getItem('keep-screen-on'); if (savedKeepScreenOn !== null) { this.keepScreenOn.set(savedKeepScreenOn === 'true'); + } else { + this.keepScreenOn.set(true); } const savedComunitaEnabled = localStorage.getItem('comunita-enabled'); if (savedComunitaEnabled !== null) { this.comunitaEnabled.set(savedComunitaEnabled === 'true'); + } else { + this.comunitaEnabled.set(false); } const savedStats = localStorage.getItem('invio-dati-statistici'); if (savedStats !== null) { this.invioDatiStatistici.set(savedStats === 'true'); + } else { + this.invioDatiStatistici.set(false); } const savedShowTags = localStorage.getItem('show-tags-in-list'); if (savedShowTags !== null) { this.showTagsInList.set(savedShowTags === 'true'); + } else { + this.showTagsInList.set(true); } const savedShowUpdateDate = localStorage.getItem('show-update-date'); if (savedShowUpdateDate !== null) { this.showUpdateDate.set(savedShowUpdateDate === 'true'); + } else { + this.showUpdateDate.set(true); } const savedStandardAutoscroll = localStorage.getItem('enable-standard-autoscroll'); if (savedStandardAutoscroll !== null) { this.enableStandardAutoscroll.set(savedStandardAutoscroll === 'true'); + } else { + this.enableStandardAutoscroll.set(true); } const savedAcousticAutoscroll = localStorage.getItem('enable-acoustic-autoscroll'); if (savedAcousticAutoscroll !== null) { this.enableAcousticAutoscroll.set(savedAcousticAutoscroll === 'true'); + } else { + this.enableAcousticAutoscroll.set(false); } // Sync browser fullscreen state with listeners (supporting vendor prefixes) diff --git a/src/app/services/theme.service.ts b/src/app/services/theme.service.ts index 0ac1853..573b5bd 100644 --- a/src/app/services/theme.service.ts +++ b/src/app/services/theme.service.ts @@ -4,24 +4,26 @@ import { Injectable, signal, effect } from '@angular/core'; providedIn: 'root' }) export class ThemeService { - public highContrast = signal(false); + public highContrast = signal(true); constructor() { // Load from localStorage const saved = localStorage.getItem('high-contrast'); - if (saved === 'true') { - this.highContrast.set(true); + if (saved !== null) { + this.highContrast.set(saved === 'true'); } else { - this.highContrast.set(false); + this.highContrast.set(true); } // Effect to apply class to body effect(() => { const isHigh = this.highContrast(); - if (isHigh) { - document.body.classList.add('high-contrast'); - } else { - document.body.classList.remove('high-contrast'); + if (typeof document !== 'undefined' && document.body) { + if (isHigh) { + document.body.classList.add('high-contrast'); + } else { + document.body.classList.remove('high-contrast'); + } } localStorage.setItem('high-contrast', isHigh.toString()); }); diff --git a/src/app/version.ts b/src/app/version.ts index bd32f35..b7db90d 100644 --- a/src/app/version.ts +++ b/src/app/version.ts @@ -1 +1 @@ -export const VERSION = '2026.05.20.1624'; +export const VERSION = '2026.05.22.0110'; diff --git a/src/assets/icons/icon-128x128.png b/src/assets/icons/icon-128x128.png index b4f8114..f9375e1 100644 Binary files a/src/assets/icons/icon-128x128.png and b/src/assets/icons/icon-128x128.png differ diff --git a/src/assets/icons/icon-144x144.png b/src/assets/icons/icon-144x144.png index b4f8114..37894af 100644 Binary files a/src/assets/icons/icon-144x144.png and b/src/assets/icons/icon-144x144.png differ diff --git a/src/assets/icons/icon-152x152.png b/src/assets/icons/icon-152x152.png index b4f8114..a856095 100644 Binary files a/src/assets/icons/icon-152x152.png and b/src/assets/icons/icon-152x152.png differ diff --git a/src/assets/icons/icon-192x192.png b/src/assets/icons/icon-192x192.png index b4f8114..750473c 100644 Binary files a/src/assets/icons/icon-192x192.png and b/src/assets/icons/icon-192x192.png differ diff --git a/src/assets/icons/icon-384x384.png b/src/assets/icons/icon-384x384.png index b4f8114..d05a3d5 100644 Binary files a/src/assets/icons/icon-384x384.png and b/src/assets/icons/icon-384x384.png differ diff --git a/src/assets/icons/icon-512x512.png b/src/assets/icons/icon-512x512.png index b4f8114..f5a59bc 100644 Binary files a/src/assets/icons/icon-512x512.png and b/src/assets/icons/icon-512x512.png differ diff --git a/src/assets/icons/icon-72x72.png b/src/assets/icons/icon-72x72.png index b4f8114..61c6986 100644 Binary files a/src/assets/icons/icon-72x72.png and b/src/assets/icons/icon-72x72.png differ diff --git a/src/assets/icons/icon-96x96.png b/src/assets/icons/icon-96x96.png index b4f8114..81dea00 100644 Binary files a/src/assets/icons/icon-96x96.png and b/src/assets/icons/icon-96x96.png differ diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index b04cd0e..fceaae4 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,4 +1,5 @@ export const environment = { production: true, - contactEmail: 'info@canticristiani.it' + contactEmail: 'info@canticristiani.it', + appName: 'CantiCristiani' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index b190756..2321f7c 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -4,7 +4,8 @@ export const environment = { production: false, - contactEmail: 'info@canticristiani.it' + contactEmail: 'info@canticristiani.it', + appName: 'CantiCristiani' }; /* diff --git a/src/main.ts b/src/main.ts index d8ea2c3..91ec6da 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,12 @@ +import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.log(err));