Compare commits

...

1 Commits

Author SHA1 Message Date
David Frassi 171780bf87 Spostato gruppo avanzamento karaoke accanto ai controlli testa nel dettaglio canto 2026-08-09 08:28:03 +02:00
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 { ThemeService } from './services/theme.service';
import { CantiService } from './services/canti.service'; import { CantiService } from './services/canti.service';
import { SettingsService } from './services/settings.service'; import { SettingsService } from './services/settings.service';
import { VERSION } from './version'; import { VERSION } from './version';
import { Router, ActivatedRoute, NavigationStart } from '@angular/router'; 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 { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter, first } from 'rxjs/operators'; import { filter, first } from 'rxjs/operators';
import { App } from '@capacitor/app'; import { App } from '@capacitor/app';
@@ -96,8 +97,12 @@ export class AppComponent implements OnInit {
private platform = inject(Platform); private platform = inject(Platform);
private router = inject(Router); private router = inject(Router);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private location = inject(Location);
private navCtrl = inject(NavController);
private toastCtrl = inject(ToastController); private toastCtrl = inject(ToastController);
private alertCtrl = inject(AlertController);
private swUpdate = inject(SwUpdate); private swUpdate = inject(SwUpdate);
private ngZone = inject(NgZone);
public showRedirectOverlay = signal<boolean>(false); public showRedirectOverlay = signal<boolean>(false);
public showInstallOverlay = signal<boolean>(false); public showInstallOverlay = signal<boolean>(false);
@@ -107,6 +112,9 @@ export class AppComponent implements OnInit {
public redirectFailed = signal<boolean>(false); public redirectFailed = signal<boolean>(false);
public protocolLink = ''; public protocolLink = '';
constructor() { constructor() {
// Gli aggiornamenti automatici e periodici sono stati rimossi. // Gli aggiornamenti automatici e periodici sono stati rimossi.
// L'aggiornamento viene gestito esclusivamente in modo manuale // L'aggiornamento viene gestito esclusivamente in modo manuale
@@ -208,24 +216,44 @@ export class AppComponent implements OnInit {
} }
async ngOnInit() { async ngOnInit() {
// Gestione tasto back per PWA/Browser (intercettando popstate di Angular Router) // Gestione del tasto back (PWA/Browser e Nativo/Hardware) secondo le 3 specifiche:
this.router.events.subscribe(event => { // 1- Se siamo su un canto (/player o /display), premendo back andiamo sempre sulla home.
if (event instanceof NavigationStart && event.navigationTrigger === 'popstate') { // 2- Se siamo sulla home (/home o /), premendo back l'app deve uscire.
const targetUrl = event.url.split('?')[0]; // 3- Per tutto il resto, segue la logica standard andando alla pagina precedente nello storico.
if (targetUrl !== '/home' && targetUrl !== '/') {
this.router.navigate(['/home'], { replaceUrl: true }); // 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) // Gestione tasto indietro hardware (es. Android / Capacitor / PWA)
this.platform.backButton.subscribeWithPriority(9999, () => { this.platform.backButton.subscribeWithPriority(10, async () => {
const currentUrl = this.router.url; const currentPath = this.router.url.split('?')[0];
const path = currentUrl.split('?')[0]; if (currentPath === '/home' || currentPath === '/') {
if (path !== '/home' && path !== '/') { // Spec 2: Sulla home, usciamo dall'app
this.router.navigate(['/home']); 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 { } 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; return true;
} else { } else {
console.log('[PWA-Update] Versione allineata, nessun aggiornamento necessario.'); console.log('[PWA-Update] Versione allineata, nessun aggiornamento necessario.');
return false;
} }
} }
} catch (e) { } catch (e) {
console.warn('[PWA-Update] version.json check fallito:', 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; return false;
} }
@@ -566,6 +528,7 @@ export class AppComponent implements OnInit {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone; const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
if (isStandalone) { if (isStandalone) {
localStorage.setItem('pwa-installed', 'true'); localStorage.setItem('pwa-installed', 'true');
this.checkLoaderDismissal();
return; return;
} }
@@ -728,6 +691,20 @@ export class AppComponent implements OnInit {
this.checkLoaderDismissal(); 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() { export function showFullscreenUpdateOverlay() {
@@ -776,3 +753,4 @@ export function showFullscreenUpdateOverlay() {
} }
}; };
} }
+4 -10
View File
@@ -12,7 +12,7 @@
</div> </div>
</ion-title> </ion-title>
<ion-buttons slot="end"> <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-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button> </ion-button>
<ng-container *ngIf="hasUpdateAvailable(); else showQrBtn"> <ng-container *ngIf="hasUpdateAvailable(); else showQrBtn">
@@ -201,13 +201,7 @@
</div> </div>
</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> </div>
<!-- Filter Card (default closed) --> <!-- Filter Card (default closed) -->
@@ -545,8 +539,8 @@
</div> </div>
</div> </div>
<!-- Delete Button (only for local songs) --> <!-- Delete Button (only for "mio" songs when "Miei" filter is active) -->
<div *ngIf="canto.id.startsWith('my_')" class="delete-section" (click)="$event.stopPropagation()"> <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-button fill="clear" color="danger" (click)="deleteMyCanto(canto.id, $event)">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon> <ion-icon slot="icon-only" name="trash-outline"></ion-icon>
</ion-button> </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) // Filter by Validati (non personali e non contrassegnati come non validati)
if (this.showValidati()) { 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) // 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;"> <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;"> <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;"> <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;" <span class="canto-number" *ngIf="canto()?.id_canti" style="flex-shrink: 0;"
[style.fontSize.rem]="0.85 * fontSize()" [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 *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>{{ canto()?.titolo || 'Player' }}</span>
</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> </div>
<!-- Row 2: Author --> <!-- Row 2: Author -->
@@ -242,11 +245,27 @@
</div> </div>
</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) --> <!-- Action Buttons (edit, condividi, accordi) -->
<div class="group"> <div class="group">
<ion-button fill="clear" size="small" (click)="editOrCloneCanto()" *ngIf="settingsService.showEditor()"> <ion-button fill="clear" size="small" (click)="editOrCloneCanto()" *ngIf="settingsService.showEditor()">
<ion-icon slot="icon-only" name="create-outline" color="secondary"></ion-icon> <ion-icon slot="icon-only" name="create-outline" color="secondary"></ion-icon>
</ion-button> </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-button fill="clear" size="small" (click)="shareCanto()">
<ion-icon slot="icon-only" name="share-social-outline" color="secondary"></ion-icon> <ion-icon slot="icon-only" name="share-social-outline" color="secondary"></ion-icon>
</ion-button> </ion-button>
@@ -261,19 +280,6 @@
</ion-button> </ion-button>
</div> </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) --> <!-- Transposition (Only in chords mode) -->
<div class="group" *ngIf="showChords()"> <div class="group" *ngIf="showChords()">
<ion-button fill="clear" size="small" (click)="transposeDown()"> <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 { .title-main {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -283,6 +298,7 @@
white-space: normal; white-space: normal;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-word; word-break: break-word;
letter-spacing: 3px;
&.active { &.active {
color: var(--ion-color-secondary); color: var(--ion-color-secondary);
+25 -3
View File
@@ -1015,7 +1015,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
if (this.isLandscapeActive()) { if (this.isLandscapeActive()) {
this.isBlackScreen.set(true); this.isBlackScreen.set(true);
} }
this.router.navigate(['/player'], { queryParams: { id: nextId } }); this.router.navigate(['/player'], { queryParams: { id: nextId }, replaceUrl: true });
} else { } else {
this.playlistService.autoPlayPlaylist.set(false); this.playlistService.autoPlayPlaylist.set(false);
} }
@@ -1033,7 +1033,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
if (this.isLandscapeActive()) { if (this.isLandscapeActive()) {
this.isBlackScreen.set(true); 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([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { id }, queryParams: { id },
queryParamsHandling: 'merge' queryParamsHandling: 'merge',
replaceUrl: true
}); });
this.restart(); 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() { async shareCanto() {
const c = this.canto(); const c = this.canto();
if (!c) return; if (!c) return;
@@ -629,6 +629,7 @@ body.high-contrast :host ::ng-deep {
white-space: normal; white-space: normal;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-word; word-break: break-word;
letter-spacing: 3px;
} }
.chord-segment { .chord-segment {
+1
View File
@@ -983,6 +983,7 @@ export class PlaylistService {
display: block; display: block;
margin-bottom: 7px; margin-bottom: 7px;
min-height: 1.2em; min-height: 1.2em;
letter-spacing: 3px;
} }
.chord-segment { .chord-segment {
display: inline-flex; display: inline-flex;
+1 -1
View File
@@ -1 +1 @@
export const VERSION = '2026.08.08.1614'; export const VERSION = '2026.08.09.0826';