Spostato gruppo avanzamento karaoke accanto ai controlli testa nel dettaglio canto

This commit is contained in:
David Frassi
2026-08-09 08:28:03 +02:00
parent a15a6c2139
commit 171780bf87
9 changed files with 130 additions and 112 deletions
+61 -83
View File
@@ -1,10 +1,11 @@
import { Component, inject, OnInit, signal, effect } from '@angular/core';
import { Component, inject, NgZone, OnInit, signal, effect } from '@angular/core';
import { ThemeService } from './services/theme.service';
import { CantiService } from './services/canti.service';
import { SettingsService } from './services/settings.service';
import { VERSION } from './version';
import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
import { ToastController, Platform } from '@ionic/angular';
import { ToastController, Platform, AlertController, NavController } from '@ionic/angular';
import { Location } from '@angular/common';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter, first } from 'rxjs/operators';
import { App } from '@capacitor/app';
@@ -96,8 +97,12 @@ export class AppComponent implements OnInit {
private platform = inject(Platform);
private router = inject(Router);
private route = inject(ActivatedRoute);
private location = inject(Location);
private navCtrl = inject(NavController);
private toastCtrl = inject(ToastController);
private alertCtrl = inject(AlertController);
private swUpdate = inject(SwUpdate);
private ngZone = inject(NgZone);
public showRedirectOverlay = signal<boolean>(false);
public showInstallOverlay = signal<boolean>(false);
@@ -107,6 +112,9 @@ export class AppComponent implements OnInit {
public redirectFailed = signal<boolean>(false);
public protocolLink = '';
constructor() {
// Gli aggiornamenti automatici e periodici sono stati rimossi.
// L'aggiornamento viene gestito esclusivamente in modo manuale
@@ -208,24 +216,44 @@ export class AppComponent implements OnInit {
}
async ngOnInit() {
// Gestione tasto back per PWA/Browser (intercettando popstate di Angular Router)
this.router.events.subscribe(event => {
if (event instanceof NavigationStart && event.navigationTrigger === 'popstate') {
const targetUrl = event.url.split('?')[0];
if (targetUrl !== '/home' && targetUrl !== '/') {
this.router.navigate(['/home'], { replaceUrl: true });
}
// Gestione del tasto back (PWA/Browser e Nativo/Hardware) secondo le 3 specifiche:
// 1- Se siamo su un canto (/player o /display), premendo back andiamo sempre sulla home.
// 2- Se siamo sulla home (/home o /), premendo back l'app deve uscire.
// 3- Per tutto il resto, segue la logica standard andando alla pagina precedente nello storico.
// Gestione popstate (tasto indietro browser / gesture PWA)
this.router.events.pipe(
filter((e): e is NavigationStart => e instanceof NavigationStart),
filter(e => e.navigationTrigger === 'popstate')
).subscribe(() => {
const currentPath = this.router.url.split('?')[0];
if (currentPath === '/home' || currentPath === '/') {
// Spec 2: Sulla home, usciamo dall'app
this.router.navigate(['/home'], { replaceUrl: true });
this.exitApp();
} else if (currentPath === '/player' || currentPath === '/display') {
// Spec 1: Su un canto, andiamo alla home azzerando lo stack
this.navCtrl.navigateRoot('/home', { animationDirection: 'back' });
}
// Spec 3: Per il resto (es. /settings), popstate prosegue normalmente verso la pagina precedente
});
// Gestione tasto back hardware per nativo (Capacitor/Cordova)
this.platform.backButton.subscribeWithPriority(9999, () => {
const currentUrl = this.router.url;
const path = currentUrl.split('?')[0];
if (path !== '/home' && path !== '/') {
this.router.navigate(['/home']);
// Gestione tasto indietro hardware (es. Android / Capacitor / PWA)
this.platform.backButton.subscribeWithPriority(10, async () => {
const currentPath = this.router.url.split('?')[0];
if (currentPath === '/home' || currentPath === '/') {
// Spec 2: Sulla home, usciamo dall'app
this.exitApp();
} else if (currentPath === '/player' || currentPath === '/display') {
// Spec 1: Su un canto, andiamo alla home azzerando lo stack
this.navCtrl.navigateRoot('/home', { animationDirection: 'back' });
} else {
App.exitApp();
// Spec 3: Per il resto, logica standard (pagina precedente)
if (window.history.length > 1) {
this.location.back();
} else {
this.navCtrl.navigateRoot('/home', { animationDirection: 'back' });
}
}
});
@@ -480,79 +508,13 @@ export class AppComponent implements OnInit {
return true;
} else {
console.log('[PWA-Update] Versione allineata, nessun aggiornamento necessario.');
return false;
}
}
} catch (e) {
console.warn('[PWA-Update] version.json check fallito:', e);
}
// Fallback: Controlla direttamente tramite SwUpdate se non intercettato da version.json
if (this.swUpdate.isEnabled) {
try {
console.log('[PWA-Update] Verifica aggiornamenti via SwUpdate all\'avvio...');
let activated = false;
const activateAndReload = async () => {
if (activated) return;
activated = true;
try {
await this.swUpdate.activateUpdate();
} catch (e) {
console.warn('[PWA-Update] activateUpdate fallito all\'avvio:', e);
}
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const reg of registrations) {
await reg.unregister();
}
}
if ('caches' in window) {
const keys = await caches.keys();
for (const key of keys) {
await caches.delete(key);
}
}
const url = new URL(window.location.href);
url.searchParams.set('update_cb', Date.now().toString());
window.location.replace(url.toString());
};
const sub = this.swUpdate.versionUpdates
.pipe(
filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
first()
)
.subscribe(() => {
console.log('[PWA-Update] VERSION_READY ricevuto all\'avvio, ricaricamento automatico...');
activateAndReload();
});
const hasUpdate = await Promise.race([
this.swUpdate.checkForUpdate(),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 8000))
]);
if (hasUpdate) {
console.log('[PWA-Update] Aggiornamento rilevato via SwUpdate. Applicazione automatica...');
setTimeout(() => {
console.log('[PWA-Update] Safety timeout raggiunto all\'avvio, procedo...');
sub.unsubscribe();
activateAndReload();
}, 15000);
return true;
} else {
sub.unsubscribe();
}
} catch (err) {
console.warn('[PWA-Update] Controllo SwUpdate fallito all\'avvio:', err);
}
}
return false;
}
@@ -566,6 +528,7 @@ export class AppComponent implements OnInit {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
if (isStandalone) {
localStorage.setItem('pwa-installed', 'true');
this.checkLoaderDismissal();
return;
}
@@ -728,6 +691,20 @@ export class AppComponent implements OnInit {
this.checkLoaderDismissal();
}
}
private exitApp() {
try {
App.exitApp();
} catch (e) {}
try {
if ((navigator as any)?.app?.exitApp) {
(navigator as any).app.exitApp();
}
} catch (e) {}
try {
window.close();
} catch (e) {}
}
}
export function showFullscreenUpdateOverlay() {
@@ -776,3 +753,4 @@ export function showFullscreenUpdateOverlay() {
}
};
}
+4 -10
View File
@@ -12,7 +12,7 @@
</div>
</ion-title>
<ion-buttons slot="end">
<ion-button routerLink="/propose-canto" class="add-btn" title="Crea un nuovo canto">
<ion-button routerLink="/propose-canto" class="add-btn" title="Crea un nuovo canto" *ngIf="settingsService.showEditor()">
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button>
<ng-container *ngIf="hasUpdateAvailable(); else showQrBtn">
@@ -201,13 +201,7 @@
</div>
</div>
<div class="filter-card-footer" *ngIf="hasPlaylistCardParams()">
<div></div>
<button type="button" class="clear-advanced-link outfit-font" (click)="clearPlaylistCard()">
<ion-icon name="close-circle-outline"></ion-icon>
<span>Reset liste</span>
</button>
</div>
</div>
<!-- Filter Card (default closed) -->
@@ -545,8 +539,8 @@
</div>
</div>
<!-- Delete Button (only for local songs) -->
<div *ngIf="canto.id.startsWith('my_')" class="delete-section" (click)="$event.stopPropagation()">
<!-- Delete Button (only for "mio" songs when "Miei" filter is active) -->
<div *ngIf="settingsService.showEditor() && canto.id.startsWith('my_') && showOnlyMine()" class="delete-section" (click)="$event.stopPropagation()">
<ion-button fill="clear" color="danger" (click)="deleteMyCanto(canto.id, $event)">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
</ion-button>
+1 -1
View File
@@ -423,7 +423,7 @@ export class HomePage implements OnDestroy {
// Filter by Validati (non personali e non contrassegnati come non validati)
if (this.showValidati()) {
list = list.filter(c => !c.nonValidato && !c.id.startsWith('my_'));
list = list.filter(c => !c.nonValidato && (onlyMine || !c.id.startsWith('my_')));
}
// Filter by Non-Validati (personali o esplicitamente non validati)
+20 -14
View File
@@ -2,7 +2,7 @@
<ion-toolbar class="bg-gradient top-toolbar" style="--padding-top: 8px; --padding-bottom: 8px; --padding-start: 12px; --padding-end: 12px;">
<div class="outfit-font" style="display: flex; flex-direction: column; width: 100%; gap: 2px;">
<!-- Row 1: Number + Title -->
<!-- Row 1: Number + Title + Settings -->
<div style="display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;">
<span class="canto-number" *ngIf="canto()?.id_canti" style="flex-shrink: 0;"
[style.fontSize.rem]="0.85 * fontSize()"
@@ -18,6 +18,9 @@
<span *ngIf="getCommunitySongNumber(canto())" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 6px; opacity: 0.9;">{{ getCommunitySongNumber(canto()) }}</span>
<span>{{ canto()?.titolo || 'Player' }}</span>
</span>
<ion-button routerLink="/settings" class="settings-btn" fill="clear" style="flex-shrink: 0; margin: 0; --color: var(--ion-color-secondary);">
<ion-icon slot="icon-only" name="settings-outline"></ion-icon>
</ion-button>
</div>
<!-- Row 2: Author -->
@@ -242,11 +245,27 @@
</div>
</div>
<!-- Navigation (Avanzamento Karaoke e Riparti da inizio) -->
<div class="group">
<ion-button fill="clear" size="small" (click)="restart()">
<ion-icon slot="icon-only" name="arrow-up-circle" color="secondary"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="prev()" [disabled]="currentLineIndex() === 0">
<ion-icon slot="icon-only" name="chevron-back" color="secondary"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="next()" [disabled]="currentLineIndex() === getTotalLines() - 1">
<ion-icon slot="icon-only" name="chevron-forward" color="secondary"></ion-icon>
</ion-button>
</div>
<!-- Action Buttons (edit, condividi, accordi) -->
<div class="group">
<ion-button fill="clear" size="small" (click)="editOrCloneCanto()" *ngIf="settingsService.showEditor()">
<ion-icon slot="icon-only" name="create-outline" color="secondary"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="deleteMyCanto()" *ngIf="settingsService.showEditor() && canto()?.id?.startsWith('my_')" color="danger">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="shareCanto()">
<ion-icon slot="icon-only" name="share-social-outline" color="secondary"></ion-icon>
</ion-button>
@@ -261,19 +280,6 @@
</ion-button>
</div>
<!-- Navigation -->
<div class="group">
<ion-button fill="clear" size="small" (click)="restart()">
<ion-icon slot="icon-only" name="arrow-up-circle" color="secondary"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="prev()" [disabled]="currentLineIndex() === 0">
<ion-icon slot="icon-only" name="chevron-back" color="secondary"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="next()" [disabled]="currentLineIndex() === getTotalLines() - 1">
<ion-icon slot="icon-only" name="chevron-forward" color="secondary"></ion-icon>
</ion-button>
</div>
<!-- Transposition (Only in chords mode) -->
<div class="group" *ngIf="showChords()">
<ion-button fill="clear" size="small" (click)="transposeDown()">
+16
View File
@@ -59,6 +59,21 @@
}
}
.settings-btn {
--color: var(--ion-color-secondary);
--padding-start: 0;
--padding-end: 0;
margin-left: 0;
margin-right: 0;
width: 38px;
min-width: 38px;
height: 38px;
ion-icon {
font-size: 1.6rem;
}
}
.title-main {
display: flex;
align-items: center;
@@ -283,6 +298,7 @@
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
letter-spacing: 3px;
&.active {
color: var(--ion-color-secondary);
+25 -3
View File
@@ -1015,7 +1015,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
if (this.isLandscapeActive()) {
this.isBlackScreen.set(true);
}
this.router.navigate(['/player'], { queryParams: { id: nextId } });
this.router.navigate(['/player'], { queryParams: { id: nextId }, replaceUrl: true });
} else {
this.playlistService.autoPlayPlaylist.set(false);
}
@@ -1033,7 +1033,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
if (this.isLandscapeActive()) {
this.isBlackScreen.set(true);
}
this.router.navigate(['/player'], { queryParams: { id: prevId } });
this.router.navigate(['/player'], { queryParams: { id: prevId }, replaceUrl: true });
}
}
@@ -1041,7 +1041,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { id },
queryParamsHandling: 'merge'
queryParamsHandling: 'merge',
replaceUrl: true
});
this.restart();
}
@@ -1132,6 +1133,27 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
}
async deleteMyCanto() {
const c = this.canto();
if (!c || !c.id.startsWith('my_')) return;
const alert = await this.alertCtrl.create({
header: 'Elimina Canto',
message: 'Sei sicuro di voler eliminare questo canto dai tuoi brani personali?',
buttons: [
{ text: 'Annulla', role: 'cancel' },
{
text: 'Elimina',
role: 'destructive',
handler: () => {
this.myCantiService.deleteCanto(c.id);
this.router.navigate(['/home']);
}
}
]
});
await alert.present();
}
async shareCanto() {
const c = this.canto();
if (!c) return;
@@ -629,6 +629,7 @@ body.high-contrast :host ::ng-deep {
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
letter-spacing: 3px;
}
.chord-segment {
+1
View File
@@ -983,6 +983,7 @@ export class PlaylistService {
display: block;
margin-bottom: 7px;
min-height: 1.2em;
letter-spacing: 3px;
}
.chord-segment {
display: inline-flex;
+1 -1
View File
@@ -1 +1 @@
export const VERSION = '2026.08.08.1614';
export const VERSION = '2026.08.09.0826';