Fix PWA redirect loop and installation options after uninstalling PWA
This commit is contained in:
+30
-17
@@ -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
|
||||
# --- 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"
|
||||
|
||||
# --- 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!"
|
||||
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
|
||||
|
||||
+19
-7
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"start_url": "/",
|
||||
"theme_color": "#3880ff",
|
||||
"background_color": "#ffffff",
|
||||
"launch_handler": {
|
||||
"client_mode": "focus-existing"
|
||||
},
|
||||
"protocol_handlers": [
|
||||
{
|
||||
"protocol": "web+canti",
|
||||
|
||||
+68
-51
@@ -4,82 +4,99 @@
|
||||
|
||||
<!-- PWA Redirect Overlay -->
|
||||
<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="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);">
|
||||
</div>
|
||||
|
||||
<!-- Fase di redirect normale (in attesa di apertura) -->
|
||||
<ng-container *ngIf="!redirectFailed()">
|
||||
<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.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
|
||||
Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta.
|
||||
</p>
|
||||
<!-- Se la PWA è rilevata come installata -->
|
||||
<ng-container *ngIf="isPwaInstalled()">
|
||||
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; color: #ffffff; -webkit-font-smoothing: antialiased;">Applicazione Installata</h2>
|
||||
<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;">
|
||||
La app risulta già installata sul dispositivo, chiudi il browser ed usa quella oppure disinstallala se preferisci utilizzarla da qui.
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
|
||||
<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);">
|
||||
Apri Manualmente
|
||||
</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 (click)="stayInBrowserForceUninstallCheck()"
|
||||
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 (L'ho disinstallata)
|
||||
</button>
|
||||
</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()"
|
||||
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>
|
||||
<!-- Se l'applicazione NON è installata (flusso normale di redirect / installazione guidata) -->
|
||||
<ng-container *ngIf="!isPwaInstalled()">
|
||||
<!-- Fase di redirect normale (in attesa di apertura) -->
|
||||
<ng-container *ngIf="!redirectFailed()">
|
||||
<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.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>
|
||||
<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);">
|
||||
INSTALLA APP
|
||||
Apri Manualmente
|
||||
</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()"
|
||||
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
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PWA Install Overlay -->
|
||||
<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 -->
|
||||
<div *ngIf="settingsService.isAndroid()"
|
||||
|
||||
+183
-30
@@ -26,6 +26,9 @@ export class AppComponent implements OnInit {
|
||||
|
||||
public showRedirectOverlay = 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 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<boolean> {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
</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>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Response>((_, 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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -53,6 +53,20 @@
|
||||
</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">
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
<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;">
|
||||
<input type="text"
|
||||
#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;"
|
||||
class="outfit-font"
|
||||
(keyup.enter)="saveComunitaCode(comunitaCodeInput.value)">
|
||||
|
||||
@@ -48,7 +48,12 @@ export class CantiService {
|
||||
public progress = signal<number>(0);
|
||||
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() {
|
||||
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({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,7 +54,7 @@ export class SettingsService {
|
||||
public karaokePageScrollMode = signal<boolean>(false);
|
||||
|
||||
/** 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à */
|
||||
public userUuid = signal<string>('');
|
||||
@@ -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<string | undefined> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean>(() => {
|
||||
// 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);
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const VERSION = '2026.06.17.0950';
|
||||
export const VERSION = '2026.06.18.0234';
|
||||
|
||||
Reference in New Issue
Block a user