Miglioramenti generali layout player landscape/portrait, traduzione pulsanti navigazione, gestione aggiornamenti PWA e modifiche al posizionamento del pulsante aggiungi canto

This commit is contained in:
David Frassi
2026-06-14 22:14:41 +02:00
parent d023bb7d3f
commit b4b7e10c5f
20 changed files with 1554 additions and 535 deletions
+24 -57
View File
@@ -19,9 +19,6 @@
<ion-button (click)="importPlaylist()" class="add-btn">
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
</ion-button>
<ion-button routerLink="/propose-canto" class="add-btn" *ngIf="!playlistService.selectionMode() && settingsService.showEditor()">
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button>
<ion-button routerLink="/settings" class="settings-btn">
<ion-icon slot="icon-only" name="settings-outline"></ion-icon>
</ion-button>
@@ -48,6 +45,19 @@
{{ filteredCanti().length }}
</div>
<div class="filter-buttons">
<ion-button
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
size="small"
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
class="filter-chip">
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length > 0"></ion-icon>
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length === 0"></ion-icon>
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
</ion-button>
<div class="comunita-filter-wrapper" style="position: relative; display: inline-flex; align-items: center; margin-right: 8px; z-index: 999;" *ngIf="settingsService.comunitaEnabled()">
<ion-button
[fill]="comunitaService.isFilterActive() ? 'solid' : 'outline'"
@@ -116,19 +126,6 @@
</span>
</ion-button>
<ion-button
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
size="small"
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
class="filter-chip">
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length > 0"></ion-icon>
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length === 0"></ion-icon>
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
</ion-button>
<ion-button
[fill]="showTopTen() ? 'solid' : 'outline'"
size="small"
@@ -186,8 +183,16 @@
<div
class="momento-chip glass"
[class.active]="showNonValidati()"
(click)="toggleNonValidati()">
Non Validati
(click)="toggleNonValidati()"
style="display: inline-flex; align-items: center; gap: 6px;">
<span>Non Validati</span>
<ion-icon
*ngIf="showNonValidati() && !playlistService.selectionMode() && settingsService.showEditor()"
name="add-outline"
(click)="$event.stopPropagation()"
routerLink="/propose-canto"
style="font-size: 1.1rem; color: #000; font-weight: bold; background: rgba(0,0,0,0.1); border-radius: 50%; padding: 2px;">
</ion-icon>
</div>
</ng-container>
@@ -213,28 +218,6 @@
(click)="onInteraction()">
<div class="ion-padding no-padding-top">
<!-- Premium PWA Android Install Banner -->
<div class="pwa-android-banner glass ion-margin-bottom" *ngIf="showAndroidBanner()">
<div class="banner-inner">
<div class="banner-icon-wrapper">
<ion-icon name="cloud-download-outline" class="banner-icon"></ion-icon>
</div>
<div class="banner-text-wrapper">
<h3 class="banner-title outfit-font">Installa la nostra App!</h3>
<p class="banner-subtitle outfit-font">
Accedi a tutti i canti all'istante, anche offline, direttamente dalla tua home screen.
</p>
</div>
<div class="banner-actions">
<ion-button fill="solid" size="small" color="secondary" class="install-action-btn outfit-font" (click)="installAndroidPwa()">
Installa
</ion-button>
<ion-button fill="clear" size="small" class="dismiss-action-btn" (click)="dismissAndroidBanner($event)">
<ion-icon name="close-outline"></ion-icon>
</ion-button>
</div>
</div>
</div>
<div *ngIf="cantiService.loading() && filteredCanti().length === 0" class="ion-text-center ion-padding loading-container">
<div class="loading-wrapper">
@@ -440,20 +423,4 @@
</ion-toolbar>
</ion-footer>
<!-- Premium PWA iOS Tooltip -->
<div class="pwa-ios-tooltip-wrapper" [class.has-player]="youtubePlayerService.currentCantoId()" *ngIf="showIosTooltip()">
<div class="pwa-ios-tooltip glass">
<div class="tooltip-header">
<span class="tooltip-title outfit-font">Installa su iPhone</span>
<ion-button fill="clear" size="small" class="dismiss-btn" (click)="dismissIosTooltip($event)">
<ion-icon name="close-outline"></ion-icon>
</ion-button>
</div>
<div class="tooltip-body">
<p class="tooltip-text outfit-font">
Tocca il pulsante Condividi <span class="safari-icon-inline"><ion-icon name="share-outline"></ion-icon></span> in basso e seleziona <strong>"Aggiungi a schermata Home"</strong>.
</p>
</div>
<div class="tooltip-arrow"></div>
</div>
</div>
+93 -69
View File
@@ -43,9 +43,6 @@ export class HomePage implements OnDestroy {
public isAddingSongs = signal<boolean>(false);
public reorderList = signal<any[]>([]);
public limit = signal<number>(30);
public showAndroidBanner = signal<boolean>(false);
public showIosTooltip = signal<boolean>(false);
public hasUpdateAvailable = signal<boolean>(false);
public fontSize = signal<number>(1.0);
@@ -77,6 +74,7 @@ export class HomePage implements OnDestroy {
private firstInteraction = true;
private updatePollInterval: any = null;
private queryParamsSubscription: any = null;
async checkForAppUpdate(event?: Event) {
if (event) event.stopPropagation();
@@ -117,14 +115,32 @@ export class HomePage implements OnDestroy {
await registration.update();
}
let sub: any = null;
let readyPromise: Promise<void> | undefined = undefined;
if (this.swUpdate.isEnabled) {
readyPromise = new Promise<void>((resolve) => {
sub = this.swUpdate.versionUpdates
.pipe(
filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
first()
)
.subscribe(() => {
resolve();
});
});
}
if (this.swUpdate.isEnabled) {
const swFoundUpdate = await this.swUpdate.checkForUpdate();
if (swFoundUpdate) {
await this.applyUpdateAndReload();
await this.applyUpdateAndReload(readyPromise, sub);
return true;
}
}
if (sub) sub.unsubscribe();
const versionMismatch = await this.checkVersionJson();
if (versionMismatch) {
await this.applyUpdateAndReload();
@@ -144,7 +160,7 @@ export class HomePage implements OnDestroy {
}
}
private async applyUpdateAndReload() {
private async applyUpdateAndReload(readyPromise?: Promise<void>, subscription?: any) {
const overlay = showFullscreenUpdateOverlay();
let activated = false;
@@ -164,20 +180,19 @@ export class HomePage implements OnDestroy {
}, 600);
};
if (this.swUpdate.isEnabled) {
this.swUpdate.versionUpdates
.pipe(
filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
first()
)
.subscribe(() => {
activateAndReload();
});
if (readyPromise) {
Promise.race([
readyPromise,
new Promise((resolve) => setTimeout(resolve, 25000))
]).then(() => {
if (subscription) subscription.unsubscribe();
activateAndReload();
});
} else {
setTimeout(() => {
activateAndReload();
}, 1000);
}
setTimeout(() => {
activateAndReload();
}, 6000);
}
private async checkVersionJson(): Promise<boolean> {
@@ -429,21 +444,7 @@ export class HomePage implements OnDestroy {
this.checkUpdateStatus();
}, 30000);
// Check if install prompts should be visible
const androidDismissed = localStorage.getItem('pwa-android-dismissed') === 'true';
const iosDismissed = localStorage.getItem('pwa-ios-dismissed') === 'true';
this.showAndroidBanner.set(
this.settingsService.isAndroid() &&
!this.settingsService.isStandalone() &&
!androidDismissed
);
this.showIosTooltip.set(
this.settingsService.isIos() &&
!this.settingsService.isStandalone() &&
!iosDismissed
);
// Track initial community filter state to avoid clearing during the initial run of the effect
let prevFilterActive = this.comunitaService.isFilterActive();
@@ -482,17 +483,43 @@ export class HomePage implements OnDestroy {
}
});
this.route.queryParams.subscribe(params => {
if (params['import']) {
this.handleImport(params['import']);
let queryParamsSubscribed = false;
effect(() => {
if (this.settingsService.isVersionCheckComplete() && !queryParamsSubscribed) {
queryParamsSubscribed = true;
this.queryParamsSubscription = this.route.queryParams.subscribe(params => {
if (params['import']) {
this.handleImport(params['import']);
}
if (params['playlist-uid']) {
this.handleRemotePlaylistImport(params['playlist-uid'], params['playlist-id']);
}
if (params['restore-uid']) {
this.handleRemoteRestore(params['restore-uid']);
}
});
}
if (params['playlist-uid']) {
this.handleRemotePlaylistImport(params['playlist-uid'], params['playlist-id']);
}, { allowSignalWrites: true });
let hasAutoActivatedPlaylist = false;
effect(() => {
const lastPl = this.playlistService.lastPlaylist();
const versionReady = this.settingsService.isVersionCheckComplete();
if (versionReady && lastPl && !hasAutoActivatedPlaylist) {
const params = this.route.snapshot.queryParams;
if (!params['id'] && !params['import'] && !params['playlist-uid'] && !params['restore-uid']) {
hasAutoActivatedPlaylist = true;
this.selectPlaylist(lastPl);
this.activeFilterType.set('playlist');
if (lastPl.ids && lastPl.ids.length > 0) {
this.goToCanto(lastPl.ids[0]);
}
} else {
hasAutoActivatedPlaylist = true;
}
}
if (params['restore-uid']) {
this.handleRemoteRestore(params['restore-uid']);
}
});
}, { allowSignalWrites: true });
let prevSelectionMode = false;
@@ -550,33 +577,7 @@ export class HomePage implements OnDestroy {
}, { allowSignalWrites: true });
}
dismissAndroidBanner(event?: Event) {
if (event) event.stopPropagation();
this.showAndroidBanner.set(false);
localStorage.setItem('pwa-android-dismissed', 'true');
}
dismissIosTooltip(event?: Event) {
if (event) event.stopPropagation();
this.showIosTooltip.set(false);
localStorage.setItem('pwa-ios-dismissed', 'true');
}
async installAndroidPwa() {
if (this.settingsService.deferredPrompt()) {
await this.settingsService.installPwa();
this.dismissAndroidBanner();
} else {
const toast = await this.toastCtrl.create({
message: 'Tocca il menu del browser (i tre puntini in alto a destra) e seleziona "Aggiungi a schermata Home" o "Installa app".',
duration: 6000,
position: 'bottom',
color: 'secondary',
buttons: [{ text: 'OK', role: 'cancel' }]
});
await toast.present();
}
}
async handleImport(base64: string) {
try {
@@ -631,7 +632,11 @@ export class HomePage implements OnDestroy {
if (this.playlistService.processImportJson(json)) {
this.limit.set(50); // Mostra più canti inizialmente per le playlist speciali
this.router.navigate([], { queryParams: { import: null }, queryParamsHandling: 'merge' });
this.activeFilterType.set('playlist');
await this.router.navigate([], { queryParams: { import: null }, queryParamsHandling: 'merge', replaceUrl: true });
if (json.ids && json.ids.length > 0) {
this.goToCanto(json.ids[0]);
}
}
} catch (e) {
console.error('Failed to import playlist', e);
@@ -651,6 +656,7 @@ export class HomePage implements OnDestroy {
});
await loading.present();
let hasNavigatedToCanto = false;
try {
const response = await fetch(`https://api.canticristiani.it/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
if (!response.ok) {
@@ -712,6 +718,7 @@ export class HomePage implements OnDestroy {
// Select it immediately
this.selectPlaylist(selectedPl);
this.activeFilterType.set('playlist');
await loading.dismiss();
@@ -721,6 +728,12 @@ export class HomePage implements OnDestroy {
color: 'success'
});
await toast.present();
if (selectedPl.ids && selectedPl.ids.length > 0) {
hasNavigatedToCanto = true;
await this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null }, queryParamsHandling: 'merge', replaceUrl: true });
this.goToCanto(selectedPl.ids[0]);
}
} else {
throw new Error('Formato dati non valido.');
}
@@ -734,7 +747,9 @@ export class HomePage implements OnDestroy {
});
await alert.present();
} finally {
this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null }, queryParamsHandling: 'merge' });
if (!hasNavigatedToCanto) {
this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null }, queryParamsHandling: 'merge' });
}
}
}
@@ -839,6 +854,9 @@ export class HomePage implements OnDestroy {
if (this.updatePollInterval) {
clearInterval(this.updatePollInterval);
}
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
}
onSearch(event: any) {
@@ -1408,6 +1426,12 @@ export class HomePage implements OnDestroy {
// Automatically save to the device!
this.playlistService.savePlaylist(playlistName, ids);
// Activate the playlist filter and open the first song!
this.activeFilterType.set('playlist');
if (ids.length > 0) {
this.goToCanto(ids[0]);
}
}
}