From b0bb5735c8c63e7ab111065b0283e7ef0a95065f Mon Sep 17 00:00:00 2001 From: David Frassi Date: Thu, 18 Jun 2026 02:36:52 +0200 Subject: [PATCH] Fix PWA redirect loop and installation options after uninstalling PWA --- allineacanti.sh | 49 ++-- allinealetture.sh | 26 ++- deploy_www.sh | 2 +- public/manifest.webmanifest | 3 + src/app/app.component.html | 119 +++++----- src/app/app.component.ts | 213 +++++++++++++++--- src/app/home/home.page.html | 2 +- src/app/home/home.page.scss | 11 +- src/app/home/home.page.ts | 6 + src/app/pages/player/player.page.ts | 15 +- src/app/pages/playlist/playlist.page.ts | 5 +- src/app/pages/settings/settings.page.html | 16 +- src/app/services/canti.service.ts | 9 +- .../services/lyrics-parser.service.spec.ts | 25 ++ src/app/services/lyrics-parser.service.ts | 8 +- src/app/services/media-session.service.ts | 8 + src/app/services/playlist.service.ts | 75 ++++-- src/app/services/settings.service.ts | 9 +- src/app/services/youtube-player.service.ts | 8 + src/app/version.ts | 2 +- 20 files changed, 466 insertions(+), 145 deletions(-) diff --git a/allineacanti.sh b/allineacanti.sh index 814c39e..3f8d62f 100755 --- a/allineacanti.sh +++ b/allineacanti.sh @@ -29,25 +29,38 @@ if ! curl -s -L "$SOURCE_URL" -o "$TEMP_FILE"; then exit 1 fi -# --- Rotazione file su FTP --- -echo "🔄 Rotazione file su FTP (canti.json -> canti_ex.json)..." -# Rinominiamo canti.json in canti_ex.json. -# Usiamo i percorsi assoluti per sicurezza. -curl -s -u "$FTP_USER:$FTP_PASS" \ - --ftp-pasv \ - -Q "*DELE /htdocs/api/canti_ex.json" \ - -Q "*RNFR /htdocs/api/canti.json" \ - -Q "*RNTO /htdocs/api/canti_ex.json" \ - "ftp://$FTP_HOST/" > /dev/null - -# --- Upload via FTP --- -echo "🚀 Caricamento nuovo file su $FTP_HOST/$REMOTE_PATH..." -if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then - echo "✅ Allineamento completato con successo!" +# --- Rotazione e Caricamento file (FTP o VPS) --- +if [ -n "$VPS_HOST" ]; then + echo "🔄 Rotazione file su VPS ($VPS_HOST)..." + ssh "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_PATH/api && mv $VPS_PATH/api/canti.json $VPS_PATH/api/canti_ex.json 2>/dev/null || true" + + echo "🚀 Caricamento nuovo file su VPS via SCP..." + if scp "$TEMP_FILE" "$VPS_USER@$VPS_HOST:$VPS_PATH/$REMOTE_PATH" && ssh "$VPS_USER@$VPS_HOST" "chmod 644 $VPS_PATH/$REMOTE_PATH"; then + echo "✅ Allineamento su VPS completato con successo!" + else + echo "❌ Errore durante il caricamento via SCP su VPS." + rm "$TEMP_FILE" + exit 1 + fi else - echo "❌ Errore durante il caricamento FTP." - rm "$TEMP_FILE" - exit 1 + echo "🔄 Rotazione file su FTP (canti.json -> canti_ex.json)..." + # Rinominiamo canti.json in canti_ex.json. + # Usiamo i percorsi assoluti per sicurezza. + curl -s -u "$FTP_USER:$FTP_PASS" \ + --ftp-pasv \ + -Q "*DELE /htdocs/api/canti_ex.json" \ + -Q "*RNFR /htdocs/api/canti.json" \ + -Q "*RNTO /htdocs/api/canti_ex.json" \ + "ftp://$FTP_HOST/" > /dev/null + + echo "🚀 Caricamento nuovo file su FTP ($FTP_HOST) via FTP..." + if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then + echo "✅ Allineamento su FTP completato con successo!" + else + echo "❌ Errore durante il caricamento FTP." + rm "$TEMP_FILE" + exit 1 + fi fi # Cleanup diff --git a/allinealetture.sh b/allinealetture.sh index 607b2c1..e3ecdea 100755 --- a/allinealetture.sh +++ b/allinealetture.sh @@ -29,14 +29,26 @@ if ! curl -s -u "$API_AUTH_USER:$API_AUTH_PASS" -L "$SOURCE_URL" -o "$TEMP_FILE" exit 1 fi -# --- Upload via FTP --- -echo "🚀 Caricamento nuovo file su $FTP_HOST/$REMOTE_PATH..." -if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then - echo "✅ Allineamento completato con successo!" +# --- Caricamento file (FTP o VPS) --- +if [ -n "$VPS_HOST" ]; then + echo "🚀 Caricamento nuovo file su VPS ($VPS_HOST) via SCP..." + ssh "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_PATH/api" + if scp "$TEMP_FILE" "$VPS_USER@$VPS_HOST:$VPS_PATH/$REMOTE_PATH" && ssh "$VPS_USER@$VPS_HOST" "chmod 644 $VPS_PATH/$REMOTE_PATH"; then + echo "✅ Allineamento su VPS completato con successo!" + else + echo "❌ Errore durante il caricamento via SCP su VPS." + rm "$TEMP_FILE" + exit 1 + fi else - echo "❌ Errore durante il caricamento FTP." - rm "$TEMP_FILE" - exit 1 + echo "🚀 Caricamento nuovo file su FTP ($FTP_HOST) via FTP..." + if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then + echo "✅ Allineamento su FTP completato con successo!" + else + echo "❌ Errore durante il caricamento FTP." + rm "$TEMP_FILE" + exit 1 + fi fi # Cleanup diff --git a/deploy_www.sh b/deploy_www.sh index cbe024f..54bd6fe 100755 --- a/deploy_www.sh +++ b/deploy_www.sh @@ -89,5 +89,5 @@ if [ "$TARGET" = "tophost" ]; then python3 scratch/deploy_ftp.py else echo "🚀 Upload via SSH/rsync su Contabo ($VPS_HOST) in corso..." - rsync -avz --delete www/ "$VPS_USER@$VPS_HOST:$VPS_PATH" + rsync -avz --delete --exclude 'api' www/ "$VPS_USER@$VPS_HOST:$VPS_PATH" fi diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 0117f84..a303b9e 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -6,6 +6,9 @@ "start_url": "/", "theme_color": "#3880ff", "background_color": "#ffffff", + "launch_handler": { + "client_mode": "focus-existing" + }, "protocol_handlers": [ { "protocol": "web+canti", diff --git a/src/app/app.component.html b/src/app/app.component.html index 02cdbc4..4dd1a68 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,82 +4,99 @@
+ style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: radial-gradient(circle at top left, #2d3436 0%, #121212 100%); color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 100000; font-family: 'Outfit', sans-serif; padding: 20px;">
CantiCristiani
- - -

Apertura App in corso

-

- Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta. -

+ + +

Applicazione Installata

+
+ La app risulta già installata sul dispositivo, chiudi il browser ed usa quella oppure disinstallala se preferisci utilizzarla da qui. +
- -
- - - -
-

Aggiungi a Home (iOS)

-

- Per aggiornamenti istantanei e uso offline, aggiungi l'app alla schermata Home di iOS: -

-
-
-
1
-
- Tocca il pulsante Condividi 📤 in Safari. -
-
-
-
2
-
- Scorri il menu e seleziona Aggiungi alla schermata Home ➕. -
-
-
- -
- - -
-

Installa CantiCristiani

+ + + + +

Apertura App in corso

- Installa l'applicazione sul tuo dispositivo per evitare problemi di cache del browser ed usarla offline! + Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta.

- +
+ Per evitare problemi di cache, la navigazione da browser è disattivata se la PWA è installata.
+ Se preferisci usare il browser, disinstalla l'app dal dispositivo. +
+
+
+ + + + +
+

Aggiungi a Home (iOS)

+

+ Per aggiornamenti istantanei e uso offline, aggiungi l'app alla schermata Home di iOS: +

+
+
+
1
+
+ Tocca il pulsante Condividi 📤 in Safari. +
+
+
+
2
+
+ Scorri il menu e seleziona Aggiungi alla schermata Home ➕. +
+
+
-
+ + +
+

Installa CantiCristiani

+

+ Installa l'applicazione sul tuo dispositivo per evitare problemi di cache del browser ed usarla offline! +

+
+ + +
+
+
+ style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: radial-gradient(circle at top left, #2d3436 0%, #121212 100%); color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 100000; font-family: 'Outfit', sans-serif; padding: 20px;">
(false); public showInstallOverlay = signal(false); + public isInstalling = signal(false); + public isRedirecting = signal(false); + public isPwaInstalled = signal(false); public redirectFailed = signal(false); public protocolLink = ''; @@ -45,13 +48,12 @@ export class AppComponent implements OnInit { return; } - // Se l'overlay di redirect o di installazione è mostrato, NON nascondiamo il loader iniziale - // per rimanere nella welcome page mentre l'utente sceglie. - if (this.showRedirectOverlay() || this.showInstallOverlay()) { + // Se stiamo attivamente installando o reindirizzando, NON nascondiamo il loader + if (this.isInstalling() || this.isRedirecting()) { return; } - // Altrimenti, nascondiamo il loader per far entrare l'utente nell'app + // Altrimenti, nascondiamo il loader per far entrare l'utente if ((window as any).PwaLoader) { (window as any).PwaLoader.hide(); } @@ -95,6 +97,15 @@ export class AppComponent implements OnInit { // Set signal indicating startup version check is complete this.settingsService.isVersionCheckComplete.set(true); + // Handle PWA Launch Queue if supported (focus-existing launch behavior) + if ('launchQueue' in window) { + (window as any).launchQueue.setConsumer((launchParams: any) => { + if (launchParams.targetURL) { + this.handleLaunchUrl(launchParams.targetURL); + } + }); + } + this.route.queryParams.subscribe(params => { const protocolUrl = params['url']; if (protocolUrl && protocolUrl.startsWith('web+canti:')) { @@ -120,10 +131,73 @@ export class AppComponent implements OnInit { } } }); + window.addEventListener('appinstalled', () => { + console.log('[AppComponent] PWA appinstalled event caught.'); + localStorage.setItem('pwa-installed', 'true'); + this.isPwaInstalled.set(true); + this.isInstalling.set(false); + this.isRedirecting.set(true); + this.showInstallOverlay.set(false); + this.showRedirectOverlay.set(false); + + if ((window as any).PwaLoader) { + (window as any).PwaLoader.show(); + (window as any).PwaLoader.update({ + title: 'Chiudi il browser', + desc: 'Applicazione installata con successo! Chiudi il browser e continua sulla PWA.', + isRedirect: true + }); + } + + setTimeout(() => { + window.location.href = this.protocolLink; + }, 1000); + }); this.checkAndRedirectToPwa(); } + handleLaunchUrl(urlStr: string) { + try { + const urlObj = new URL(urlStr); + + // 1. Check if it's a protocol link inside query params (e.g. /?url=web+canti://...) + const protocolUrl = urlObj.searchParams.get('url'); + if (protocolUrl && protocolUrl.startsWith('web+canti:')) { + const cleanUrl = protocolUrl.replace('web+canti://', 'http://localhost/'); + const innerUrlObj = new URL(cleanUrl); + + let targetPath = innerUrlObj.pathname; + if (targetPath === '/open' || targetPath === '//open') { + targetPath = '/'; + } else if (targetPath.startsWith('/open/')) { + targetPath = targetPath.substring(5); + } + + const queryParams: any = {}; + innerUrlObj.searchParams.forEach((value, key) => { + queryParams[key] = value; + }); + + console.log('[AppComponent] Launch queue routing (protocol) to:', targetPath, queryParams); + this.router.navigate([targetPath], { queryParams, replaceUrl: true }); + return; + } + + // 2. Otherwise, route directly to the pathname and query params of the URL + let targetPath = urlObj.pathname; + const queryParams: any = {}; + urlObj.searchParams.forEach((value, key) => { + queryParams[key] = value; + }); + + console.log('[AppComponent] Launch queue routing (direct) to:', targetPath, queryParams); + this.router.navigate([targetPath], { queryParams, replaceUrl: true }); + } catch (e) { + console.error('Failed to parse launch url:', urlStr, e); + } + } + async checkVersionSync(): Promise { // Controlla SEMPRE version.json per primo — è il modo più affidabile per // rilevare un disallineamento di versione, indipendentemente dallo stato del SW. @@ -304,30 +378,49 @@ export class AppComponent implements OnInit { } } - if (isInstalled) { - const skipRedirect = sessionStorage.getItem('skip-pwa-redirect') === 'true'; - if (!skipRedirect) { - this.showRedirectOverlay.set(true); - this.redirectFailed.set(false); - if ((window as any).PwaLoader) { - (window as any).PwaLoader.update({ isRedirect: true }); + // Se non è rilevata in localStorage/relatedApps ed è Android o Desktop con supporto ai prompt: + // attendiamo 1.5s per dare tempo all'evento 'beforeinstallprompt' di scattare. + // Se non scatta, significa che l'app è già installata. + if (!isInstalled && !this.settingsService.isIos() && ('onbeforeinstallprompt' in window)) { + await new Promise(resolve => setTimeout(resolve, 1500)); + isInstalled = localStorage.getItem('pwa-installed') === 'true'; + if (!isInstalled) { + const hasPrompt = this.settingsService.showInstallButton() || this.settingsService.deferredPrompt(); + if (!hasPrompt) { + console.log('[AppComponent] PWA detected as already installed (onbeforeinstallprompt supported but no prompt fired).'); + isInstalled = true; + localStorage.setItem('pwa-installed', 'true'); } - // Tentiamo il reindirizzamento automatico - setTimeout(() => { - window.location.href = this.protocolLink; - - // Se dopo 2 secondi l'utente è ancora qui, probabilmente l'app non è installata - setTimeout(() => { - if (this.showRedirectOverlay()) { - this.redirectFailed.set(true); - localStorage.setItem('pwa-installed', 'false'); - if ((window as any).PwaLoader) { - (window as any).PwaLoader.hide(); - } - } - }, 2000); - }, 800); } + } + + this.isPwaInstalled.set(isInstalled); + + if (isInstalled) { + if (sessionStorage.getItem('skip-pwa-redirect') === 'true') { + this.showRedirectOverlay.set(false); + this.checkLoaderDismissal(); + return; + } + this.showRedirectOverlay.set(true); + this.redirectFailed.set(false); + if ((window as any).PwaLoader) { + (window as any).PwaLoader.update({ isRedirect: true }); + } + // Tentiamo il reindirizzamento automatico + setTimeout(() => { + window.location.href = this.protocolLink; + + // Se dopo 2 secondi l'utente è ancora qui, mostriamo lo stato fallito per aprire manualmente o indicare la disinstallazione + setTimeout(() => { + if (this.showRedirectOverlay()) { + this.redirectFailed.set(true); + if ((window as any).PwaLoader) { + (window as any).PwaLoader.hide(); + } + } + }, 2000); + }, 800); } else { // Se non è installata, proponiamo l'installazione immediata per evitare la cache del browser e avere un'esperienza ottimale const skipInstall = sessionStorage.getItem('skip-pwa-install') === 'true'; @@ -335,6 +428,8 @@ export class AppComponent implements OnInit { const hasDesktopPrompt = this.settingsService.showInstallButton() || this.settingsService.deferredPrompt(); if (!skipInstall && (isMobile || hasDesktopPrompt)) { this.showInstallOverlay.set(true); + } else { + this.checkLoaderDismissal(); } } } @@ -360,22 +455,80 @@ export class AppComponent implements OnInit { stayInBrowser() { sessionStorage.setItem('skip-pwa-redirect', 'true'); this.showRedirectOverlay.set(false); + this.checkLoaderDismissal(); + } + + stayInBrowserForceUninstallCheck() { + localStorage.setItem('pwa-installed', 'false'); + this.isPwaInstalled.set(false); + this.stayInBrowser(); } closeInstallOverlay() { sessionStorage.setItem('skip-pwa-install', 'true'); this.showInstallOverlay.set(false); + this.checkLoaderDismissal(); } async triggerInstall() { - await this.settingsService.installPwa(); - this.closeInstallOverlay(); + this.isInstalling.set(true); + this.showInstallOverlay.set(false); + if ((window as any).PwaLoader) { + (window as any).PwaLoader.show(); + (window as any).PwaLoader.update({ + title: 'Installazione in corso...', + desc: 'Completa l\'installazione tramite la finestra del browser.' + }); + } + + const outcome = await this.settingsService.installPwa(); + if (outcome === 'accepted') { + this.isInstalling.set(false); + this.isRedirecting.set(true); + if ((window as any).PwaLoader) { + (window as any).PwaLoader.update({ + title: 'Apertura Applicazione...', + desc: 'Installazione completata! Chiudi il browser e continua sulla PWA.', + isRedirect: true + }); + } + } else { + this.isInstalling.set(false); + this.isRedirecting.set(false); + this.showInstallOverlay.set(true); + this.checkLoaderDismissal(); + } } async triggerInstallFromRedirect() { - await this.settingsService.installPwa(); - sessionStorage.setItem('skip-pwa-redirect', 'true'); + this.isInstalling.set(true); this.showRedirectOverlay.set(false); + if ((window as any).PwaLoader) { + (window as any).PwaLoader.show(); + (window as any).PwaLoader.update({ + title: 'Installazione in corso...', + desc: 'Completa l\'installazione tramite la finestra del browser.' + }); + } + + const outcome = await this.settingsService.installPwa(); + if (outcome === 'accepted') { + sessionStorage.setItem('skip-pwa-redirect', 'true'); + this.isInstalling.set(false); + this.isRedirecting.set(true); + if ((window as any).PwaLoader) { + (window as any).PwaLoader.update({ + title: 'Apertura Applicazione...', + desc: 'Installazione completata! Chiudi il browser e continua sulla PWA.', + isRedirect: true + }); + } + } else { + this.isInstalling.set(false); + this.isRedirecting.set(false); + this.showRedirectOverlay.set(true); + this.checkLoaderDismissal(); + } } } diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index c12bb49..fa29d81 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -379,7 +379,7 @@
- + diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index adffa4d..dbd2a09 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -148,10 +148,9 @@ ion-item.glass { --padding-start: 16px; --inner-padding-end: 16px; margin-bottom: 12px; - transition: transform 0.2s ease, background 0.2s ease; + transition: background 0.2s ease; &:active { - transform: scale(0.98); --background: rgba(255, 255, 255, 0.1); } } @@ -426,7 +425,8 @@ ion-title { width: 100%; .selection-column { - padding: 10px 14px 10px 0; + padding: 12px 18px 12px 12px; + margin-left: -12px; flex-shrink: 0; display: flex; align-items: center; @@ -443,6 +443,11 @@ ion-title { padding: 10px 0; cursor: pointer; overflow: hidden; + transition: transform 0.2s ease; + + &:active { + transform: scale(0.98); + } .top-row { display: flex; diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 5073cb5..94aca6e 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -427,6 +427,9 @@ export class HomePage implements OnDestroy { if (this.playlistService.selectionMode() && !this.isAddingSongs()) { return this.filteredCanti(); } + if (this.comunitaService.isFilterActive() && this.comunitaService.comunitaCode()) { + return this.filteredCanti(); + } return this.filteredCanti().slice(0, this.limit()); }); @@ -1647,6 +1650,9 @@ export class HomePage implements OnDestroy { const songSettings = pl ? pl.songSettings : undefined; await this.playlistService.savePlaylist(data.name, ids, songSettings); + // Azzoppa / azzera il filtro cerca libera quando si salva + this.searchQuery.set(''); + const toast = await this.toastCtrl.create({ message: 'Playlist salvata!', duration: 2000, diff --git a/src/app/pages/player/player.page.ts b/src/app/pages/player/player.page.ts index fb63311..81a453a 100644 --- a/src/app/pages/player/player.page.ts +++ b/src/app/pages/player/player.page.ts @@ -1072,8 +1072,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { // Try to fetch YouTube thumbnail first if available if (thumbUrl) { try { + // Use images.weserv.nl as a CORS proxy to bypass YouTube's CORS policy + const proxiedUrl = `https://images.weserv.nl/?url=${encodeURIComponent(thumbUrl)}`; const response = await Promise.race([ - fetch(thumbUrl), + fetch(proxiedUrl), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000)) ]); if (response.ok) { @@ -1083,10 +1085,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } catch (e) { console.warn('[Share] Failed to fetch YouTube thumbnail due to CORS or timeout:', e); } - } - - // If no YouTube thumb or fetch failed, fallback to local canticristiani logo (favicon) - if (!fileToShare) { + } else { + // If no YouTube thumb is available, fallback to local canticristiani logo (favicon) try { const response = await fetch('assets/icon/favicon.png'); if (response.ok) { @@ -1106,7 +1106,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { url: shareLink }; - if (fileToShare && navigator.canShare && navigator.canShare({ files: [fileToShare] })) { + const isMac = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + + if (!isMac && fileToShare && navigator.canShare && navigator.canShare({ files: [fileToShare] })) { shareDataObj.files = [fileToShare]; } diff --git a/src/app/pages/playlist/playlist.page.ts b/src/app/pages/playlist/playlist.page.ts index ef002db..57f0b2a 100644 --- a/src/app/pages/playlist/playlist.page.ts +++ b/src/app/pages/playlist/playlist.page.ts @@ -178,7 +178,10 @@ export class PlaylistPage { const blob = await res.blob(); const file = new File([blob], fileName, { type: 'image/png' }); - if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { + const isMac = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + + if (!isMac && navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: 'Playlist CantiCristiani', diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index 54d7769..c9d99fa 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -53,6 +53,20 @@ + +
+ + + +

Come installare l'applicazione

+

+ Se usi Chrome / Edge / Brave: puoi installarla cliccando sull'icona di installazione o 📥 che appare a destra nella barra degli indirizzi del browser.

+ Se usi Safari (su Mac): clicca sul menu File in alto e seleziona Aggiungi al Dock.... +

+
+
+
+
@@ -175,7 +189,7 @@
diff --git a/src/app/services/canti.service.ts b/src/app/services/canti.service.ts index 56da78c..27daea9 100644 --- a/src/app/services/canti.service.ts +++ b/src/app/services/canti.service.ts @@ -48,7 +48,12 @@ export class CantiService { public progress = signal(0); public firstLoadCompleted = signal(false); - private API_URL = 'https://www.canticristiani.it/api/canti.json'; + private getApiUrl(): string { + const isProduction = window.location.hostname.includes('canticristiani.it'); + return isProduction + ? `${window.location.origin}/api/canti.json` + : 'https://www.canticristiani.it/api/canti.json'; + } constructor() { this.init(); @@ -92,7 +97,7 @@ export class CantiService { this.loading.set(true); this.progress.set(0); - this.http.get(`${this.API_URL}?t=${Date.now()}`, { + this.http.get(`${this.getApiUrl()}?t=${Date.now()}`, { reportProgress: true, observe: 'events' }).subscribe({ diff --git a/src/app/services/lyrics-parser.service.spec.ts b/src/app/services/lyrics-parser.service.spec.ts index 963c386..d598ea4 100644 --- a/src/app/services/lyrics-parser.service.spec.ts +++ b/src/app/services/lyrics-parser.service.spec.ts @@ -101,4 +101,29 @@ Rallegri*amoci,* expect(sections[1].lines[0].text).toBe('Rallegriamoci,'); expect(sections[1].lines[0].segments[0].chord).toBeUndefined(); }); + it('should format sections outside of soc/eoc tags as verses', () => { + const rawSong = ` +{c:Intro:} [RE] + +[RE]Guardami Signor +{soc} +Abba Padre! +{eoc} +[RE]Più solo non sarò +`; + const sections = service.parseAccordi(rawSong); + // There should be three sections: + // 1. Verse (Intro & Guardami Signor) + // 2. Chorus (Abba Padre) + // 3. Verse (Più solo non sarò) + expect(sections.length).toBe(3); + expect(sections[0].type).toBe('verse'); + expect(sections[0].lines[0].text).toBe('Intro '); + expect(sections[0].lines[0].segments[0].text).toBe('Intro '); + expect(sections[0].lines[0].segments[0].chord).toBeUndefined(); + expect(sections[0].lines[0].segments[1].chord).toBe('RE'); + expect(sections[0].lines[1].text).toBe('Guardami Signor'); + expect(sections[1].type).toBe('chorus'); + expect(sections[2].type).toBe('verse'); + }); }); diff --git a/src/app/services/lyrics-parser.service.ts b/src/app/services/lyrics-parser.service.ts index 6fb7fd9..5658c86 100644 --- a/src/app/services/lyrics-parser.service.ts +++ b/src/app/services/lyrics-parser.service.ts @@ -116,11 +116,12 @@ export class LyricsParserService { currentLines = []; currentRawLines = []; currentAction = null; + currentType = 'verse'; continue; } // Skip structural tags (already handled above) - if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + if (trimmed.startsWith('{') && trimmed.endsWith('}') && !trimmed.startsWith('{c:') && !trimmed.startsWith('{comment:')) { continue; } @@ -148,6 +149,11 @@ export class LyricsParserService { } } + // Clean comment tags: {c:Text} or {comment:Text} -> Text (removing trailing colon if any) + resolvedLine = resolvedLine.replace(/\{(?:c|comment):([^}]+)\}/g, (match, p1) => { + return p1.trim().replace(/:$/, ''); + }); + currentRawLines.push(line); // Parse line diff --git a/src/app/services/media-session.service.ts b/src/app/services/media-session.service.ts index 73ca27d..6db3dac 100644 --- a/src/app/services/media-session.service.ts +++ b/src/app/services/media-session.service.ts @@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { CantiService } from './canti.service'; import { MyCantiService } from './my-canti.service'; import { ComunitaService } from './comunita.service'; +import { PlaylistService } from './playlist.service'; @Injectable({ providedIn: 'root' @@ -10,6 +11,7 @@ export class MediaSessionService { private cantiService = inject(CantiService); private myCantiService = inject(MyCantiService); private comunitaService = inject(ComunitaService); + private playlistService = inject(PlaylistService); public updateMetadata(cantoId: string) { if (!('mediaSession' in navigator)) return; @@ -21,6 +23,12 @@ export class MediaSessionService { if (!canto) { canto = this.comunitaService.comunitaCantiPersonali().find(c => c.id === cantoId); } + if (!canto) { + canto = this.playlistService.remoteCustomSongs().find(c => c.id === cantoId || String(c.id_canti) === cantoId); + } + if (!canto) { + canto = this.playlistService.remoteShareCanti().find(c => c.id === cantoId || String(c.id_canti) === cantoId); + } if (!canto) return; const thumb = this.cantiService.getYoutubeThumb(canto.link_youtube) || 'assets/icons/icon-512x512.png'; diff --git a/src/app/services/playlist.service.ts b/src/app/services/playlist.service.ts index 6d7c6f6..951789c 100644 --- a/src/app/services/playlist.service.ts +++ b/src/app/services/playlist.service.ts @@ -386,6 +386,34 @@ export class PlaylistService { }); }); + this.remoteCustomSongs().forEach(c => { + const id_canti = c.id_canti || Date.now(); + mergedSongsMap.set(id_canti, { + id_canti, + titolo: c.titolo || 'Senza Titolo', + autore: c.autore || '', + link_youtube: c.link_youtube || '', + accordi: c.accordi || '', + momenti: c.id_momenti?.map((id: any) => String(id)) || [], + periodi: [] as string[], + testo: c.testo || '' + }); + }); + + this.comunitaService.comunitaCantiPersonali().forEach(c => { + const id_canti = c.id_canti || Date.now(); + mergedSongsMap.set(id_canti, { + id_canti, + titolo: c.titolo || 'Senza Titolo', + autore: c.autore || '', + link_youtube: c.link_youtube || '', + accordi: c.accordi || '', + momenti: c.id_momenti?.map((id: any) => String(id)) || [], + periodi: [] as string[], + testo: c.testo || '' + }); + }); + const customSongs = Array.from(mergedSongsMap.values()); const playlistSongs = this.playlists().map(pl => ({ @@ -570,32 +598,43 @@ export class PlaylistService { const blob = await res.blob(); const file = new File([blob], fileName, { type: 'image/png' }); - if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { + const isMac = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + + if (!isMac && navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: 'Playlist CantiCristiani', text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla subito: ${shareLink}` }); } else { - // Fallback: copia il link negli appunti e scarica l'immagine del QR - try { - if (navigator.clipboard) { - await navigator.clipboard.writeText(shareLink); - const toast = await this.toastCtrl.create({ - message: 'Link playlist copiato negli appunti! QR Code scaricato.', - duration: 3000, - color: 'success' - }); - await toast.present(); + // Fallback to simple share text or copy/download + if (navigator.share) { + await navigator.share({ + title: 'Playlist CantiCristiani', + text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla: ${shareLink}` + }); + } else { + // Fallback: copia il link negli appunti e scarica l'immagine del QR + try { + if (navigator.clipboard) { + await navigator.clipboard.writeText(shareLink); + const toast = await this.toastCtrl.create({ + message: 'Link playlist copiato negli appunti! QR Code scaricato.', + duration: 3000, + color: 'success' + }); + await toast.present(); + } + } catch (clipErr) { + console.warn('Failed to copy link to clipboard:', clipErr); } - } catch (clipErr) { - console.warn('Failed to copy link to clipboard:', clipErr); - } - const link = document.createElement('a'); - link.href = qrImage; - link.download = fileName; - link.click(); + const link = document.createElement('a'); + link.href = qrImage; + link.download = fileName; + link.click(); + } } } catch (err) { console.error('Share failed', err); diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 4db7955..c4e90fa 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -54,7 +54,7 @@ export class SettingsService { public karaokePageScrollMode = signal(false); /** Vista orizzontale per proiezione: true = attiva layout landscape per proiezione */ - public landscapeProjectionEnabled = signal(true); + public landscapeProjectionEnabled = signal(false); /** Identificativo utente univoco per la gestione delle comunità */ public userUuid = signal(''); @@ -251,7 +251,7 @@ export class SettingsService { if (savedLandscapeProjectionEnabled !== null) { this.landscapeProjectionEnabled.set(savedLandscapeProjectionEnabled === 'true'); } else { - this.landscapeProjectionEnabled.set(true); + this.landscapeProjectionEnabled.set(false); } // Sync browser fullscreen state with listeners (supporting vendor prefixes) @@ -464,10 +464,10 @@ export class SettingsService { localStorage.setItem('user-name', trimmed); } - async installPwa() { + async installPwa(): Promise { const promptEvent = this.deferredPrompt(); if (!promptEvent) { - return; + return undefined; } // Show the install prompt promptEvent.prompt(); @@ -477,5 +477,6 @@ export class SettingsService { // We've used the prompt, and can't use it again, discard it this.deferredPrompt.set(null); this.showInstallButton.set(false); + return outcome; } } diff --git a/src/app/services/youtube-player.service.ts b/src/app/services/youtube-player.service.ts index 3ebaa37..806825d 100644 --- a/src/app/services/youtube-player.service.ts +++ b/src/app/services/youtube-player.service.ts @@ -5,6 +5,7 @@ import { MediaSessionService } from './media-session.service'; import { MyCantiService } from './my-canti.service'; import { ConnectivityService } from './connectivity.service'; import { ComunitaService } from './comunita.service'; +import { PlaylistService } from './playlist.service'; @Injectable({ providedIn: 'root' @@ -16,6 +17,7 @@ export class YoutubePlayerService { private myCantiService = inject(MyCantiService); private connectivityService = inject(ConnectivityService); private comunitaService = inject(ComunitaService); + private playlistService = inject(PlaylistService); public isPlayerSupported = computed(() => { // Check if offline @@ -127,6 +129,12 @@ export class YoutubePlayerService { if (!canto) { canto = this.comunitaService.comunitaCantiPersonali().find(c => c.id === cantoId); } + if (!canto) { + canto = this.playlistService.remoteCustomSongs().find(c => c.id === cantoId || String(c.id_canti) === cantoId); + } + if (!canto) { + canto = this.playlistService.remoteShareCanti().find(c => c.id === cantoId || String(c.id_canti) === cantoId); + } if (!canto) return; const videoId = this.cantiService.getYoutubeId(canto.link_youtube); diff --git a/src/app/version.ts b/src/app/version.ts index 808817b..40758e0 100644 --- a/src/app/version.ts +++ b/src/app/version.ts @@ -1 +1 @@ -export const VERSION = '2026.06.17.0950'; +export const VERSION = '2026.06.18.0234';