Fix PWA redirect loop and installation options after uninstalling PWA

This commit is contained in:
David Frassi
2026-06-18 02:36:52 +02:00
parent 93385097f2
commit b0bb5735c8
20 changed files with 466 additions and 145 deletions
+31 -18
View File
@@ -29,25 +29,38 @@ if ! curl -s -L "$SOURCE_URL" -o "$TEMP_FILE"; then
exit 1 exit 1
fi fi
# --- Rotazione file su FTP --- # --- Rotazione e Caricamento file (FTP o VPS) ---
echo "🔄 Rotazione file su FTP (canti.json -> canti_ex.json)..." if [ -n "$VPS_HOST" ]; then
# Rinominiamo canti.json in canti_ex.json. echo "🔄 Rotazione file su VPS ($VPS_HOST)..."
# Usiamo i percorsi assoluti per sicurezza. 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"
curl -s -u "$FTP_USER:$FTP_PASS" \
--ftp-pasv \ echo "🚀 Caricamento nuovo file su VPS via SCP..."
-Q "*DELE /htdocs/api/canti_ex.json" \ if scp "$TEMP_FILE" "$VPS_USER@$VPS_HOST:$VPS_PATH/$REMOTE_PATH" && ssh "$VPS_USER@$VPS_HOST" "chmod 644 $VPS_PATH/$REMOTE_PATH"; then
-Q "*RNFR /htdocs/api/canti.json" \ echo "✅ Allineamento su VPS completato con successo!"
-Q "*RNTO /htdocs/api/canti_ex.json" \ else
"ftp://$FTP_HOST/" > /dev/null echo "❌ Errore durante il caricamento via SCP su VPS."
rm "$TEMP_FILE"
# --- Upload via FTP --- exit 1
echo "🚀 Caricamento nuovo file su $FTP_HOST/$REMOTE_PATH..." fi
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!"
else else
echo "❌ Errore durante il caricamento FTP." echo "🔄 Rotazione file su FTP (canti.json -> canti_ex.json)..."
rm "$TEMP_FILE" # Rinominiamo canti.json in canti_ex.json.
exit 1 # 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 fi
# Cleanup # Cleanup
+19 -7
View File
@@ -29,14 +29,26 @@ if ! curl -s -u "$API_AUTH_USER:$API_AUTH_PASS" -L "$SOURCE_URL" -o "$TEMP_FILE"
exit 1 exit 1
fi fi
# --- Upload via FTP --- # --- Caricamento file (FTP o VPS) ---
echo "🚀 Caricamento nuovo file su $FTP_HOST/$REMOTE_PATH..." if [ -n "$VPS_HOST" ]; then
if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then echo "🚀 Caricamento nuovo file su VPS ($VPS_HOST) via SCP..."
echo "✅ Allineamento completato con successo!" 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 else
echo "❌ Errore durante il caricamento FTP." echo "🚀 Caricamento nuovo file su FTP ($FTP_HOST) via FTP..."
rm "$TEMP_FILE" if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then
exit 1 echo "✅ Allineamento su FTP completato con successo!"
else
echo "❌ Errore durante il caricamento FTP."
rm "$TEMP_FILE"
exit 1
fi
fi fi
# Cleanup # Cleanup
+1 -1
View File
@@ -89,5 +89,5 @@ if [ "$TARGET" = "tophost" ]; then
python3 scratch/deploy_ftp.py python3 scratch/deploy_ftp.py
else else
echo "🚀 Upload via SSH/rsync su Contabo ($VPS_HOST) in corso..." 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 fi
+3
View File
@@ -6,6 +6,9 @@
"start_url": "/", "start_url": "/",
"theme_color": "#3880ff", "theme_color": "#3880ff",
"background_color": "#ffffff", "background_color": "#ffffff",
"launch_handler": {
"client_mode": "focus-existing"
},
"protocol_handlers": [ "protocol_handlers": [
{ {
"protocol": "web+canti", "protocol": "web+canti",
+68 -51
View File
@@ -4,82 +4,99 @@
<!-- PWA Redirect Overlay --> <!-- PWA Redirect Overlay -->
<div *ngIf="showRedirectOverlay()" <div *ngIf="showRedirectOverlay()"
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(18, 18, 18, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 100000; font-family: 'Outfit', sans-serif; padding: 20px;"> 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;">
<div style="text-align: center; padding: 30px; max-width: 420px; width: 100%; background: rgba(30, 30, 30, 0.75); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);"> <div style="text-align: center; padding: 30px; max-width: 420px; width: 100%; background: rgba(30, 30, 30, 0.75); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);">
<div style="margin-bottom: 25px; display: inline-block;"> <div style="margin-bottom: 25px; display: inline-block;">
<img src="assets/icon/favicon.png" alt="CantiCristiani" style="width: 80px; height: 80px; border-radius: 20px; box-shadow: 0 8px 24px rgba(230, 126, 34, 0.4); border: 2px solid rgba(230, 126, 34, 0.2);"> <img src="assets/icon/favicon.png" alt="CantiCristiani" style="width: 80px; height: 80px; border-radius: 20px; box-shadow: 0 8px 24px rgba(230, 126, 34, 0.4); border: 2px solid rgba(230, 126, 34, 0.2);">
</div> </div>
<!-- Fase di redirect normale (in attesa di apertura) --> <!-- Se la PWA è rilevata come installata -->
<ng-container *ngIf="!redirectFailed()"> <ng-container *ngIf="isPwaInstalled()">
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Apertura App in corso</h2> <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; color: #ffffff; -webkit-font-smoothing: antialiased;">Applicazione Installata</h2>
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;"> <div style="font-size: 1.05rem; color: rgba(255, 255, 255, 0.9); line-height: 1.6; -webkit-font-smoothing: antialiased; text-align: center; padding: 0 10px; margin-bottom: 25px;">
Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta. La app risulta già installata sul dispositivo, chiudi il browser ed usa quella oppure disinstallala se preferisci utilizzarla da qui.
</p> </div>
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;"> <div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
<button (click)="openPwaManual()" <button (click)="stayInBrowserForceUninstallCheck()"
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);"> style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; width: 100%;">
Apri Manualmente Continua nel browser (L'ho disinstallata)
</button>
<button (click)="stayInBrowser()"
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease;">
Rimani nel browser
</button> </button>
</div> </div>
</ng-container> </ng-container>
<!-- Se l'apertura automatica/manuale fallisce (PWA probabilmente non installata) --> <!-- Se l'applicazione NON è installata (flusso normale di redirect / installazione guidata) -->
<ng-container *ngIf="redirectFailed()"> <ng-container *ngIf="!isPwaInstalled()">
<!-- iOS Guide --> <!-- Fase di redirect normale (in attesa di apertura) -->
<div *ngIf="settingsService.isIos()" style="text-align: left;"> <ng-container *ngIf="!redirectFailed()">
<h2 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; text-align: center; -webkit-font-smoothing: antialiased;">Aggiungi a Home (iOS)</h2> <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Apertura App in corso</h2>
<p style="font-size: 0.9rem; color: rgba(255, 255, 255, 0.65); line-height: 1.4; text-align: center; margin-bottom: 20px; -webkit-font-smoothing: antialiased;">
Per aggiornamenti istantanei e uso offline, aggiungi l'app alla schermata Home di iOS:
</p>
<div style="display: flex; flex-direction: column; gap: 16px; margin-bottom: 25px; color: rgba(255, 255, 255, 0.85); font-size: 0.95rem;">
<div style="display: flex; align-items: center; gap: 15px;">
<div style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(230, 126, 34, 0.15); border-radius: 50%; color: #e67e22; font-weight: bold; flex-shrink: 0;">1</div>
<div style="flex-grow: 1; -webkit-font-smoothing: antialiased;">
Tocca il pulsante <strong>Condividi</strong> <span style="display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border-radius: 6px; padding: 4px; font-size: 1.1rem; vertical-align: middle;">📤</span> in Safari.
</div>
</div>
<div style="display: flex; align-items: center; gap: 15px;">
<div style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(230, 126, 34, 0.15); border-radius: 50%; color: #e67e22; font-weight: bold; flex-shrink: 0;">2</div>
<div style="flex-grow: 1; -webkit-font-smoothing: antialiased;">
Scorri il menu e seleziona <strong>Aggiungi alla schermata Home</strong> .
</div>
</div>
</div>
<button (click)="stayInBrowser()"
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25); width: 100%; text-align: center;">
Continua nel browser
</button>
</div>
<!-- Android / Desktop -->
<div *ngIf="!settingsService.isIos()" style="text-align: center;">
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Installa CantiCristiani</h2>
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;"> <p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
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.
</p> </p>
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;"> <div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
<button *ngIf="settingsService.showInstallButton() || settingsService.deferredPrompt()" (click)="triggerInstallFromRedirect()" <button (click)="openPwaManual()"
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);"> style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);">
INSTALLA APP Apri Manualmente
</button> </button>
<div style="margin-top: 15px; font-size: 0.85rem; color: rgba(255, 255, 255, 0.5); line-height: 1.5; -webkit-font-smoothing: antialiased; padding: 0 10px; text-align: center;">
Per evitare problemi di cache, la navigazione da browser è disattivata se la PWA è installata.<br>
Se preferisci usare il browser, disinstalla l'app dal dispositivo.
</div>
</div>
</ng-container>
<!-- Se l'apertura automatica/manuale fallisce (PWA probabilmente non installata) -->
<ng-container *ngIf="redirectFailed()">
<!-- iOS Guide -->
<div *ngIf="settingsService.isIos()" style="text-align: left;">
<h2 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; text-align: center; -webkit-font-smoothing: antialiased;">Aggiungi a Home (iOS)</h2>
<p style="font-size: 0.9rem; color: rgba(255, 255, 255, 0.65); line-height: 1.4; text-align: center; margin-bottom: 20px; -webkit-font-smoothing: antialiased;">
Per aggiornamenti istantanei e uso offline, aggiungi l'app alla schermata Home di iOS:
</p>
<div style="display: flex; flex-direction: column; gap: 16px; margin-bottom: 25px; color: rgba(255, 255, 255, 0.85); font-size: 0.95rem;">
<div style="display: flex; align-items: center; gap: 15px;">
<div style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(230, 126, 34, 0.15); border-radius: 50%; color: #e67e22; font-weight: bold; flex-shrink: 0;">1</div>
<div style="flex-grow: 1; -webkit-font-smoothing: antialiased;">
Tocca il pulsante <strong>Condividi</strong> <span style="display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border-radius: 6px; padding: 4px; font-size: 1.1rem; vertical-align: middle;">📤</span> in Safari.
</div>
</div>
<div style="display: flex; align-items: center; gap: 15px;">
<div style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(230, 126, 34, 0.15); border-radius: 50%; color: #e67e22; font-weight: bold; flex-shrink: 0;">2</div>
<div style="flex-grow: 1; -webkit-font-smoothing: antialiased;">
Scorri il menu e seleziona <strong>Aggiungi alla schermata Home</strong> .
</div>
</div>
</div>
<button (click)="stayInBrowser()" <button (click)="stayInBrowser()"
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; width: 100%;"> style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25); width: 100%; text-align: center;">
Continua nel browser Continua nel browser
</button> </button>
</div> </div>
</div>
<!-- Android / Desktop -->
<div *ngIf="!settingsService.isIos()" style="text-align: center;">
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Installa CantiCristiani</h2>
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
Installa l'applicazione sul tuo dispositivo per evitare problemi di cache del browser ed usarla offline!
</p>
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
<button *ngIf="settingsService.showInstallButton() || settingsService.deferredPrompt()" (click)="triggerInstallFromRedirect()"
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);">
INSTALLA APP
</button>
<button (click)="stayInBrowser()"
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; width: 100%;">
Continua nel browser
</button>
</div>
</div>
</ng-container>
</ng-container> </ng-container>
</div> </div>
</div> </div>
<!-- PWA Install Overlay --> <!-- PWA Install Overlay -->
<div *ngIf="showInstallOverlay()" <div *ngIf="showInstallOverlay()"
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(18, 18, 18, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 100000; font-family: 'Outfit', sans-serif; padding: 20px;"> 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;">
<!-- Android Install Prompt --> <!-- Android Install Prompt -->
<div *ngIf="settingsService.isAndroid()" <div *ngIf="settingsService.isAndroid()"
+183 -30
View File
@@ -26,6 +26,9 @@ export class AppComponent implements OnInit {
public showRedirectOverlay = signal<boolean>(false); public showRedirectOverlay = signal<boolean>(false);
public showInstallOverlay = signal<boolean>(false); public showInstallOverlay = signal<boolean>(false);
public isInstalling = signal<boolean>(false);
public isRedirecting = signal<boolean>(false);
public isPwaInstalled = signal<boolean>(false);
public redirectFailed = signal<boolean>(false); public redirectFailed = signal<boolean>(false);
public protocolLink = ''; public protocolLink = '';
@@ -45,13 +48,12 @@ export class AppComponent implements OnInit {
return; return;
} }
// Se l'overlay di redirect o di installazione è mostrato, NON nascondiamo il loader iniziale // Se stiamo attivamente installando o reindirizzando, NON nascondiamo il loader
// per rimanere nella welcome page mentre l'utente sceglie. if (this.isInstalling() || this.isRedirecting()) {
if (this.showRedirectOverlay() || this.showInstallOverlay()) {
return; 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) { if ((window as any).PwaLoader) {
(window as any).PwaLoader.hide(); (window as any).PwaLoader.hide();
} }
@@ -95,6 +97,15 @@ export class AppComponent implements OnInit {
// Set signal indicating startup version check is complete // Set signal indicating startup version check is complete
this.settingsService.isVersionCheckComplete.set(true); 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 => { this.route.queryParams.subscribe(params => {
const protocolUrl = params['url']; const protocolUrl = params['url'];
if (protocolUrl && protocolUrl.startsWith('web+canti:')) { 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(); 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<boolean> { async checkVersionSync(): Promise<boolean> {
// Controlla SEMPRE version.json per primo — è il modo più affidabile per // Controlla SEMPRE version.json per primo — è il modo più affidabile per
// rilevare un disallineamento di versione, indipendentemente dallo stato del SW. // rilevare un disallineamento di versione, indipendentemente dallo stato del SW.
@@ -304,30 +378,49 @@ export class AppComponent implements OnInit {
} }
} }
if (isInstalled) { // Se non è rilevata in localStorage/relatedApps ed è Android o Desktop con supporto ai prompt:
const skipRedirect = sessionStorage.getItem('skip-pwa-redirect') === 'true'; // attendiamo 1.5s per dare tempo all'evento 'beforeinstallprompt' di scattare.
if (!skipRedirect) { // Se non scatta, significa che l'app è già installata.
this.showRedirectOverlay.set(true); if (!isInstalled && !this.settingsService.isIos() && ('onbeforeinstallprompt' in window)) {
this.redirectFailed.set(false); await new Promise(resolve => setTimeout(resolve, 1500));
if ((window as any).PwaLoader) { isInstalled = localStorage.getItem('pwa-installed') === 'true';
(window as any).PwaLoader.update({ isRedirect: 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 { } else {
// Se non è installata, proponiamo l'installazione immediata per evitare la cache del browser e avere un'esperienza ottimale // 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'; 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(); const hasDesktopPrompt = this.settingsService.showInstallButton() || this.settingsService.deferredPrompt();
if (!skipInstall && (isMobile || hasDesktopPrompt)) { if (!skipInstall && (isMobile || hasDesktopPrompt)) {
this.showInstallOverlay.set(true); this.showInstallOverlay.set(true);
} else {
this.checkLoaderDismissal();
} }
} }
} }
@@ -360,22 +455,80 @@ export class AppComponent implements OnInit {
stayInBrowser() { stayInBrowser() {
sessionStorage.setItem('skip-pwa-redirect', 'true'); sessionStorage.setItem('skip-pwa-redirect', 'true');
this.showRedirectOverlay.set(false); this.showRedirectOverlay.set(false);
this.checkLoaderDismissal();
}
stayInBrowserForceUninstallCheck() {
localStorage.setItem('pwa-installed', 'false');
this.isPwaInstalled.set(false);
this.stayInBrowser();
} }
closeInstallOverlay() { closeInstallOverlay() {
sessionStorage.setItem('skip-pwa-install', 'true'); sessionStorage.setItem('skip-pwa-install', 'true');
this.showInstallOverlay.set(false); this.showInstallOverlay.set(false);
this.checkLoaderDismissal();
} }
async triggerInstall() { async triggerInstall() {
await this.settingsService.installPwa(); this.isInstalling.set(true);
this.closeInstallOverlay(); 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() { async triggerInstallFromRedirect() {
await this.settingsService.installPwa(); this.isInstalling.set(true);
sessionStorage.setItem('skip-pwa-redirect', 'true');
this.showRedirectOverlay.set(false); 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();
}
} }
} }
+1 -1
View File
@@ -379,7 +379,7 @@
</div> </div>
</div> </div>
<ion-infinite-scroll (ionInfinite)="loadData($event)" threshold="150px" [disabled]="limit() >= filteredCanti().length || (playlistService.selectionMode() && !isAddingSongs())"> <ion-infinite-scroll (ionInfinite)="loadData($event)" threshold="150px" [disabled]="(comunitaService.isFilterActive() && comunitaService.comunitaCode()) || limit() >= filteredCanti().length || (playlistService.selectionMode() && !isAddingSongs())">
<ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Caricamento altri canti..."> <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Caricamento altri canti...">
</ion-infinite-scroll-content> </ion-infinite-scroll-content>
</ion-infinite-scroll> </ion-infinite-scroll>
+8 -3
View File
@@ -148,10 +148,9 @@ ion-item.glass {
--padding-start: 16px; --padding-start: 16px;
--inner-padding-end: 16px; --inner-padding-end: 16px;
margin-bottom: 12px; margin-bottom: 12px;
transition: transform 0.2s ease, background 0.2s ease; transition: background 0.2s ease;
&:active { &:active {
transform: scale(0.98);
--background: rgba(255, 255, 255, 0.1); --background: rgba(255, 255, 255, 0.1);
} }
} }
@@ -426,7 +425,8 @@ ion-title {
width: 100%; width: 100%;
.selection-column { .selection-column {
padding: 10px 14px 10px 0; padding: 12px 18px 12px 12px;
margin-left: -12px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -443,6 +443,11 @@ ion-title {
padding: 10px 0; padding: 10px 0;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
transition: transform 0.2s ease;
&:active {
transform: scale(0.98);
}
.top-row { .top-row {
display: flex; display: flex;
+6
View File
@@ -427,6 +427,9 @@ export class HomePage implements OnDestroy {
if (this.playlistService.selectionMode() && !this.isAddingSongs()) { if (this.playlistService.selectionMode() && !this.isAddingSongs()) {
return this.filteredCanti(); return this.filteredCanti();
} }
if (this.comunitaService.isFilterActive() && this.comunitaService.comunitaCode()) {
return this.filteredCanti();
}
return this.filteredCanti().slice(0, this.limit()); return this.filteredCanti().slice(0, this.limit());
}); });
@@ -1647,6 +1650,9 @@ export class HomePage implements OnDestroy {
const songSettings = pl ? pl.songSettings : undefined; const songSettings = pl ? pl.songSettings : undefined;
await this.playlistService.savePlaylist(data.name, ids, songSettings); 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({ const toast = await this.toastCtrl.create({
message: 'Playlist salvata!', message: 'Playlist salvata!',
duration: 2000, duration: 2000,
+9 -6
View File
@@ -1072,8 +1072,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
// Try to fetch YouTube thumbnail first if available // Try to fetch YouTube thumbnail first if available
if (thumbUrl) { if (thumbUrl) {
try { 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([ const response = await Promise.race([
fetch(thumbUrl), fetch(proxiedUrl),
new Promise<Response>((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000)) new Promise<Response>((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
]); ]);
if (response.ok) { if (response.ok) {
@@ -1083,10 +1085,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
} catch (e) { } catch (e) {
console.warn('[Share] Failed to fetch YouTube thumbnail due to CORS or timeout:', e); console.warn('[Share] Failed to fetch YouTube thumbnail due to CORS or timeout:', e);
} }
} } else {
// If no YouTube thumb is available, fallback to local canticristiani logo (favicon)
// If no YouTube thumb or fetch failed, fallback to local canticristiani logo (favicon)
if (!fileToShare) {
try { try {
const response = await fetch('assets/icon/favicon.png'); const response = await fetch('assets/icon/favicon.png');
if (response.ok) { if (response.ok) {
@@ -1106,7 +1106,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
url: shareLink 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]; shareDataObj.files = [fileToShare];
} }
+4 -1
View File
@@ -178,7 +178,10 @@ export class PlaylistPage {
const blob = await res.blob(); const blob = await res.blob();
const file = new File([blob], fileName, { type: 'image/png' }); 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({ await navigator.share({
files: [file], files: [file],
title: 'Playlist CantiCristiani', title: 'Playlist CantiCristiani',
+15 -1
View File
@@ -53,6 +53,20 @@
</div> </div>
</div> </div>
<!-- PWA Install Group (Desktop Fallback Instructions) -->
<div class="settings-group glass ion-margin-bottom" *ngIf="!settingsService.showInstallButton() && !settingsService.isStandalone() && !settingsService.isIos()">
<ion-item class="transparent-item" lines="none">
<ion-icon name="download-outline" slot="start" color="secondary"></ion-icon>
<ion-label class="outfit-font" style="white-space: normal;">
<h2 class="settings-item-title" style="color: var(--ion-color-secondary); font-weight: bold; margin-bottom: 6px;">Come installare l'applicazione</h2>
<p class="settings-item-subtitle" style="font-size: 0.85rem; line-height: 1.45; opacity: 0.9; color: var(--ion-text-color); margin: 0;">
Se usi <strong>Chrome / Edge / Brave</strong>: puoi installarla cliccando sull'icona di installazione <span style="display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border-radius: 6px; padding: 2px 6px; font-size: 0.9rem; vertical-align: middle;"></span> o <span style="display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border-radius: 6px; padding: 2px 6px; font-size: 0.9rem; vertical-align: middle;">📥</span> che appare a destra nella barra degli indirizzi del browser.<br><br>
Se usi <strong>Safari (su Mac)</strong>: clicca sul menu <strong>File</strong> in alto e seleziona <strong>Aggiungi al Dock...</strong>.
</p>
</ion-label>
</ion-item>
</div>
<div class="settings-group glass ion-margin-bottom"> <div class="settings-group glass ion-margin-bottom">
<ion-item class="transparent-item" lines="none"> <ion-item class="transparent-item" lines="none">
<ion-icon name="scan-outline" slot="start" color="secondary"></ion-icon> <ion-icon name="scan-outline" slot="start" color="secondary"></ion-icon>
@@ -175,7 +189,7 @@
<div class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 8px 12px; border-radius: 10px; flex-grow: 1; display: flex; align-items: center;"> <div class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 8px 12px; border-radius: 10px; flex-grow: 1; display: flex; align-items: center;">
<input type="text" <input type="text"
#comunitaCodeInput #comunitaCodeInput
placeholder="Es: 123456" placeholder="il codice di 6 cifre"
style="background: transparent; border: none; color: var(--ion-text-color); font-size: 0.85rem; width: 100%; outline: none;" style="background: transparent; border: none; color: var(--ion-text-color); font-size: 0.85rem; width: 100%; outline: none;"
class="outfit-font" class="outfit-font"
(keyup.enter)="saveComunitaCode(comunitaCodeInput.value)"> (keyup.enter)="saveComunitaCode(comunitaCodeInput.value)">
+7 -2
View File
@@ -48,7 +48,12 @@ export class CantiService {
public progress = signal<number>(0); public progress = signal<number>(0);
public firstLoadCompleted = signal<boolean>(false); public firstLoadCompleted = signal<boolean>(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() { constructor() {
this.init(); this.init();
@@ -92,7 +97,7 @@ export class CantiService {
this.loading.set(true); this.loading.set(true);
this.progress.set(0); this.progress.set(0);
this.http.get(`${this.API_URL}?t=${Date.now()}`, { this.http.get(`${this.getApiUrl()}?t=${Date.now()}`, {
reportProgress: true, reportProgress: true,
observe: 'events' observe: 'events'
}).subscribe({ }).subscribe({
@@ -101,4 +101,29 @@ Rallegri*amoci,*
expect(sections[1].lines[0].text).toBe('Rallegriamoci,'); expect(sections[1].lines[0].text).toBe('Rallegriamoci,');
expect(sections[1].lines[0].segments[0].chord).toBeUndefined(); 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');
});
}); });
+7 -1
View File
@@ -116,11 +116,12 @@ export class LyricsParserService {
currentLines = []; currentLines = [];
currentRawLines = []; currentRawLines = [];
currentAction = null; currentAction = null;
currentType = 'verse';
continue; continue;
} }
// Skip structural tags (already handled above) // Skip structural tags (already handled above)
if (trimmed.startsWith('{') && trimmed.endsWith('}')) { if (trimmed.startsWith('{') && trimmed.endsWith('}') && !trimmed.startsWith('{c:') && !trimmed.startsWith('{comment:')) {
continue; 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); currentRawLines.push(line);
// Parse line // Parse line
@@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core';
import { CantiService } from './canti.service'; import { CantiService } from './canti.service';
import { MyCantiService } from './my-canti.service'; import { MyCantiService } from './my-canti.service';
import { ComunitaService } from './comunita.service'; import { ComunitaService } from './comunita.service';
import { PlaylistService } from './playlist.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -10,6 +11,7 @@ export class MediaSessionService {
private cantiService = inject(CantiService); private cantiService = inject(CantiService);
private myCantiService = inject(MyCantiService); private myCantiService = inject(MyCantiService);
private comunitaService = inject(ComunitaService); private comunitaService = inject(ComunitaService);
private playlistService = inject(PlaylistService);
public updateMetadata(cantoId: string) { public updateMetadata(cantoId: string) {
if (!('mediaSession' in navigator)) return; if (!('mediaSession' in navigator)) return;
@@ -21,6 +23,12 @@ export class MediaSessionService {
if (!canto) { if (!canto) {
canto = this.comunitaService.comunitaCantiPersonali().find(c => c.id === cantoId); 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; if (!canto) return;
const thumb = this.cantiService.getYoutubeThumb(canto.link_youtube) || 'assets/icons/icon-512x512.png'; const thumb = this.cantiService.getYoutubeThumb(canto.link_youtube) || 'assets/icons/icon-512x512.png';
+57 -18
View File
@@ -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 customSongs = Array.from(mergedSongsMap.values());
const playlistSongs = this.playlists().map(pl => ({ const playlistSongs = this.playlists().map(pl => ({
@@ -570,32 +598,43 @@ export class PlaylistService {
const blob = await res.blob(); const blob = await res.blob();
const file = new File([blob], fileName, { type: 'image/png' }); 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({ await navigator.share({
files: [file], files: [file],
title: 'Playlist CantiCristiani', title: 'Playlist CantiCristiani',
text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla subito: ${shareLink}` text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla subito: ${shareLink}`
}); });
} else { } else {
// Fallback: copia il link negli appunti e scarica l'immagine del QR // Fallback to simple share text or copy/download
try { if (navigator.share) {
if (navigator.clipboard) { await navigator.share({
await navigator.clipboard.writeText(shareLink); title: 'Playlist CantiCristiani',
const toast = await this.toastCtrl.create({ text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla: ${shareLink}`
message: 'Link playlist copiato negli appunti! QR Code scaricato.', });
duration: 3000, } else {
color: 'success' // Fallback: copia il link negli appunti e scarica l'immagine del QR
}); try {
await toast.present(); 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'); const link = document.createElement('a');
link.href = qrImage; link.href = qrImage;
link.download = fileName; link.download = fileName;
link.click(); link.click();
}
} }
} catch (err) { } catch (err) {
console.error('Share failed', err); console.error('Share failed', err);
+5 -4
View File
@@ -54,7 +54,7 @@ export class SettingsService {
public karaokePageScrollMode = signal<boolean>(false); public karaokePageScrollMode = signal<boolean>(false);
/** Vista orizzontale per proiezione: true = attiva layout landscape per proiezione */ /** Vista orizzontale per proiezione: true = attiva layout landscape per proiezione */
public landscapeProjectionEnabled = signal<boolean>(true); public landscapeProjectionEnabled = signal<boolean>(false);
/** Identificativo utente univoco per la gestione delle comunità */ /** Identificativo utente univoco per la gestione delle comunità */
public userUuid = signal<string>(''); public userUuid = signal<string>('');
@@ -251,7 +251,7 @@ export class SettingsService {
if (savedLandscapeProjectionEnabled !== null) { if (savedLandscapeProjectionEnabled !== null) {
this.landscapeProjectionEnabled.set(savedLandscapeProjectionEnabled === 'true'); this.landscapeProjectionEnabled.set(savedLandscapeProjectionEnabled === 'true');
} else { } else {
this.landscapeProjectionEnabled.set(true); this.landscapeProjectionEnabled.set(false);
} }
// Sync browser fullscreen state with listeners (supporting vendor prefixes) // Sync browser fullscreen state with listeners (supporting vendor prefixes)
@@ -464,10 +464,10 @@ export class SettingsService {
localStorage.setItem('user-name', trimmed); localStorage.setItem('user-name', trimmed);
} }
async installPwa() { async installPwa(): Promise<string | undefined> {
const promptEvent = this.deferredPrompt(); const promptEvent = this.deferredPrompt();
if (!promptEvent) { if (!promptEvent) {
return; return undefined;
} }
// Show the install prompt // Show the install prompt
promptEvent.prompt(); promptEvent.prompt();
@@ -477,5 +477,6 @@ export class SettingsService {
// We've used the prompt, and can't use it again, discard it // We've used the prompt, and can't use it again, discard it
this.deferredPrompt.set(null); this.deferredPrompt.set(null);
this.showInstallButton.set(false); this.showInstallButton.set(false);
return outcome;
} }
} }
@@ -5,6 +5,7 @@ import { MediaSessionService } from './media-session.service';
import { MyCantiService } from './my-canti.service'; import { MyCantiService } from './my-canti.service';
import { ConnectivityService } from './connectivity.service'; import { ConnectivityService } from './connectivity.service';
import { ComunitaService } from './comunita.service'; import { ComunitaService } from './comunita.service';
import { PlaylistService } from './playlist.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -16,6 +17,7 @@ export class YoutubePlayerService {
private myCantiService = inject(MyCantiService); private myCantiService = inject(MyCantiService);
private connectivityService = inject(ConnectivityService); private connectivityService = inject(ConnectivityService);
private comunitaService = inject(ComunitaService); private comunitaService = inject(ComunitaService);
private playlistService = inject(PlaylistService);
public isPlayerSupported = computed<boolean>(() => { public isPlayerSupported = computed<boolean>(() => {
// Check if offline // Check if offline
@@ -127,6 +129,12 @@ export class YoutubePlayerService {
if (!canto) { if (!canto) {
canto = this.comunitaService.comunitaCantiPersonali().find(c => c.id === cantoId); 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; if (!canto) return;
const videoId = this.cantiService.getYoutubeId(canto.link_youtube); const videoId = this.cantiService.getYoutubeId(canto.link_youtube);
+1 -1
View File
@@ -1 +1 @@
export const VERSION = '2026.06.17.0950'; export const VERSION = '2026.06.18.0234';