diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b639a4d..4f22289 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,6 +2,7 @@ import { Component, inject, ApplicationRef } from '@angular/core'; import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { filter, first } from 'rxjs/operators'; import { concat, interval, fromEvent } from 'rxjs'; +import { ToastController } from '@ionic/angular'; @Component({ selector: 'app-root', @@ -12,6 +13,7 @@ import { concat, interval, fromEvent } from 'rxjs'; export class AppComponent { private swUpdate = inject(SwUpdate); private appRef = inject(ApplicationRef); + private toastCtrl = inject(ToastController); constructor() { this.setupUpdates(); @@ -55,14 +57,29 @@ export class AppComponent { } }); - // 4. Activate update and reload when a new version is ready + // 4. Activate update and reload when a new version is ready (Show interactive toast) this.swUpdate.versionUpdates .pipe(filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY')) - .subscribe(() => { - console.log('[PWA-Update] New version ready! Activating and reloading...'); - this.swUpdate.activateUpdate().then(() => { - window.location.reload(); + .subscribe(async () => { + console.log('[PWA-Update] New version ready! Showing toast prompt...'); + const toast = await this.toastCtrl.create({ + message: 'Nuova versione dell\'applicazione disponibile!', + position: 'bottom', + color: 'secondary', + buttons: [ + { + text: 'Aggiorna', + role: 'cancel', + handler: () => { + console.log('[PWA-Update] Activating update and reloading...'); + this.swUpdate.activateUpdate().then(() => { + window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now()); + }); + } + } + ] }); + await toast.present(); }); } } diff --git a/src/app/components/qr-scanner/qr-scanner.component.html b/src/app/components/qr-scanner/qr-scanner.component.html index 97a871d..cc8d944 100644 --- a/src/app/components/qr-scanner/qr-scanner.component.html +++ b/src/app/components/qr-scanner/qr-scanner.component.html @@ -11,11 +11,30 @@
+

Inquadra il QR Code della tua parrocchia

+ + +
+ +
+ + +
+ +

+ Su iPad, se non vedi lo switch fotocamera, tocca l'icona "aA" in alto nella barra di Safari e seleziona "Richiedi sito mobile". +

+
diff --git a/src/app/components/qr-scanner/qr-scanner.component.scss b/src/app/components/qr-scanner/qr-scanner.component.scss index 4fb8270..0d1f105 100644 --- a/src/app/components/qr-scanner/qr-scanner.component.scss +++ b/src/app/components/qr-scanner/qr-scanner.component.scss @@ -55,6 +55,91 @@ padding: 0 40px; } } + + .camera-toggle-container { + position: absolute; + bottom: 50px; + left: 0; + right: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; + } + + .camera-toggle-btn { + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.25); + color: white; + padding: 12px 24px; + border-radius: 30px; + font-family: 'Outfit', sans-serif; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + cursor: pointer; + transition: background 0.2s, transform 0.2s, box-shadow 0.2s; + outline: none; + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + + &:active { + background: rgba(255, 255, 255, 0.35); + transform: scale(0.96); + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.37); + } + + ion-icon { + font-size: 20px; + } + } + + .ipad-warning-container { + position: absolute; + bottom: 40px; + left: 24px; + right: 24px; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 14px; + padding: 14px 18px; + display: flex; + align-items: flex-start; + gap: 12px; + color: white; + z-index: 10; + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5); + + ion-icon { + font-size: 24px; + color: var(--ion-color-secondary); + flex-shrink: 0; + margin-top: 2px; + } + + p { + margin: 0; + font-family: 'Outfit', sans-serif; + font-size: 0.82rem; + line-height: 1.45; + font-weight: 500; + text-align: left; + + strong { + color: var(--ion-color-secondary); + font-weight: 700; + } + } + } } @keyframes scan { diff --git a/src/app/components/qr-scanner/qr-scanner.component.ts b/src/app/components/qr-scanner/qr-scanner.component.ts index ef5426b..7556f9c 100644 --- a/src/app/components/qr-scanner/qr-scanner.component.ts +++ b/src/app/components/qr-scanner/qr-scanner.component.ts @@ -16,6 +16,39 @@ export class QrScannerComponent { private modalCtrl = inject(ModalController); public allowedFormats = [BarcodeFormat.QR_CODE]; + public availableDevices: MediaDeviceInfo[] = []; + public currentDevice: MediaDeviceInfo | undefined = undefined; + + onCamerasFound(devices: MediaDeviceInfo[]) { + this.availableDevices = devices; + if (devices && devices.length > 0) { + // Cerca la fotocamera posteriore (etichette contenenti 'back', 'rear', 'environment', 'posteriore') + const backCamera = devices.find(d => { + const label = d.label.toLowerCase(); + return label.includes('back') || + label.includes('rear') || + label.includes('environment') || + label.includes('posteriore'); + }); + this.currentDevice = backCamera || devices[0]; + } + } + + toggleCamera() { + if (this.availableDevices.length <= 1) return; + const currentIndex = this.availableDevices.findIndex(d => d.deviceId === this.currentDevice?.deviceId); + const nextIndex = (currentIndex + 1) % this.availableDevices.length; + this.currentDevice = this.availableDevices[nextIndex]; + } + + get showIpadWarning(): boolean { + const isIPadDesktop = + /Macintosh/.test(navigator.userAgent) && + navigator.maxTouchPoints !== undefined && + navigator.maxTouchPoints > 1; + return isIPadDesktop && this.availableDevices.length <= 1; + } + onCodeResult(result: string) { if (result) { this.modalCtrl.dismiss(result); diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index ced4088..8c074b4 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -160,7 +160,7 @@
- + diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index dc70074..7ae63f8 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -174,7 +174,7 @@ ion-title { align-items: center; justify-content: flex-start; gap: 12px; - padding: 16px 0 4px 24px; // Reduced padding to bring search bar closer + padding: 16px 0 16px 24px; // Spacing adjusted for search bar breathing room } .header-logo { @@ -318,7 +318,7 @@ ion-title { .search-wrapper-group { display: flex; flex-direction: column; - padding: 0 16px 8px 16px; + padding: 6px 16px 8px 16px; // Added slight top padding for search bar breathing room gap: 8px; } diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index adfdd7a..0b3f62f 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -131,9 +131,9 @@ export class HomePage implements OnDestroy { ...this.comunitaService.comunitaCantiPersonali() ]; - // Filter to only community canti + // Filter to only community canti, but preserve canti that are in the active playlist list = list.filter(c => { - return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id); + return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id) || activeIds.includes(c.id); }); // Deduplicate: parish-customized versions take precedence @@ -144,7 +144,7 @@ export class HomePage implements OnDestroy { if (!existing) { seen.set(key, canto); } else { - if (!existing.nonValidato && canto.nonValidato) { + if (!existing.isPersonal && canto.isPersonal) { seen.set(key, canto); } } @@ -253,6 +253,9 @@ export class HomePage implements OnDestroy { }); public visibleCanti = computed(() => { + if (this.playlistService.selectionMode() && !this.isAddingSongs()) { + return this.filteredCanti(); + } return this.filteredCanti().slice(0, this.limit()); }); @@ -393,15 +396,70 @@ export class HomePage implements OnDestroy { } } - handleImport(base64: string) { + async handleImport(base64: string) { try { const decoded = decodeURIComponent(escape(atob(base64))); const json = JSON.parse(decoded); + + if (json && json.comunitaCode) { + // 1. Abilita la funzionalità comunità nelle impostazioni + this.settingsService.comunitaEnabled.set(true); + localStorage.setItem('comunita-enabled', 'true'); + + // 2. Mostra un overlay di caricamento premium con indicatore di progresso + const loading = await this.loadingCtrl.create({ + message: 'Attivazione comunità: Caricamento 0%', + cssClass: 'premium-loading', + spinner: 'crescent' + }); + await loading.present(); + + // Iscrizione agli aggiornamenti di progresso + let progressInterval: any = null; + progressInterval = setInterval(() => { + const pct = this.comunitaService.loadingProgress(); + loading.message = `Attivazione comunità: Caricamento ${pct}%`; + if (pct >= 100) { + clearInterval(progressInterval); + } + }, 100); + + const success = await this.comunitaService.setComunitaCode(json.comunitaCode); + clearInterval(progressInterval); + await loading.dismiss(); + + if (success) { + const toast = await this.toastCtrl.create({ + message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`, + duration: 2500, + color: 'success', + position: 'bottom' + }); + await toast.present(); + } else { + const toast = await this.toastCtrl.create({ + message: 'Codice comunità non trovato o errore di connessione.', + duration: 3000, + color: 'danger', + position: 'bottom' + }); + await toast.present(); + } + } + 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' }); } } catch (e) { console.error('Failed to import playlist', e); + const toast = await this.toastCtrl.create({ + message: 'Errore durante l\'importazione della playlist.', + duration: 3000, + color: 'danger', + position: 'bottom' + }); + await toast.present(); } } @@ -847,21 +905,25 @@ export class HomePage implements OnDestroy { if (data.startsWith('canti:')) { const parts = data.replace('canti:', '').split(':'); let idsStr = ''; + let playlistName = 'Lista Parrocchiale'; if (parts.length > 1) { - this.playlistService.activeListName.set(parts[0]); + playlistName = parts[0]; idsStr = parts[1]; } else { - this.playlistService.activeListName.set('Lista Parrocchiale'); idsStr = parts[0]; } const ids = idsStr.split(',').map(id => id.trim()); this.playlistService.activeListIds.set(ids); + this.playlistService.activeListName.set(playlistName); this.limit.set(50); // Show more initially for special lists // Clear other filters to avoid confusion this.selectedLiturgico.set(null); this.selectedTematico.set(null); + + // Automatically save to the device! + this.playlistService.savePlaylist(playlistName, ids); } } @@ -936,6 +998,12 @@ export class HomePage implements OnDestroy { return !!id && id.startsWith('comunita_'); } + isActivePlaylistSaved(): boolean { + const id = this.playlistService.activePlaylistId(); + if (!id) return false; + return this.playlistService.playlists().some(p => p.id === id); + } + clearSpecialList(event?: Event) { if (event) event.stopPropagation(); this.playlistService.activeListIds.set([]); @@ -955,7 +1023,12 @@ export class HomePage implements OnDestroy { shareActivePlaylist() { const ids = this.playlistService.activeListIds(); const name = this.playlistService.activeListName() || 'Playlist'; - this.playlistService.sharePlaylistQR(ids, name); + + const id = this.playlistService.activePlaylistId(); + const pl = this.playlistService.playlists().find(p => p.id === id); + const songSettings = pl ? pl.songSettings : undefined; + + this.playlistService.sharePlaylistQR(ids, name, songSettings); } async importPlaylist() { diff --git a/src/app/pages/display/display.page.ts b/src/app/pages/display/display.page.ts index dbf0141..9e6de11 100644 --- a/src/app/pages/display/display.page.ts +++ b/src/app/pages/display/display.page.ts @@ -16,14 +16,42 @@ export class DisplayPage implements OnInit, OnDestroy { public showChords = signal(false); public fontSize = signal(1.0); public currentLineIndex = signal(0); + public transposeAmount = signal(0); public parsedSections = computed(() => { const c = this.canto(); if (!c) return []; - if (this.showChords() && c.accordi) { - return this.lyricsParser.parseAccordi(c.accordi); + + let sections: ParsedSection[]; + const hasChordsInText = !c.accordi && c.testo?.includes('['); + + if ((this.showChords() && c.accordi) || (this.showChords() && hasChordsInText)) { + sections = this.lyricsParser.parseAccordi(c.accordi || c.testo); + } else { + sections = this.lyricsParser.parseText(c.testo); } - return this.lyricsParser.parseText(c.testo); + + if (sections.length === 0 && c.testo) { + const fallbackLines = c.testo.split('\n') + .filter(l => l.trim().length > 0) + .map(l => ({ + text: l.trim(), + segments: [{ text: l.trim() }] + })); + + if (fallbackLines.length > 0) { + sections = [{ + type: 'verse', + lines: fallbackLines + }]; + } + } + + if (this.showChords()) { + return this.lyricsParser.transposeSections(sections, this.transposeAmount()); + } + + return sections; }); /** Get the current line text and surrounding lines from flat index */ @@ -94,6 +122,7 @@ export class DisplayPage implements OnInit, OnDestroy { } if (event.data.type === 'SYNC_CANTO') { this.activeSongId.set(event.data.id); + this.transposeAmount.set(0); } if (event.data.type === 'SYNC_CHORDS') { this.showChords.set(event.data.showChords); @@ -101,6 +130,9 @@ export class DisplayPage implements OnInit, OnDestroy { if (event.data.type === 'SYNC_FONT') { this.fontSize.set(event.data.fontSize); } + if (event.data.type === 'SYNC_TRANSPOSE') { + this.transposeAmount.set(event.data.amount); + } }; } diff --git a/src/app/pages/player/player.page.html b/src/app/pages/player/player.page.html index efb396f..4717d44 100644 --- a/src/app/pages/player/player.page.html +++ b/src/app/pages/player/player.page.html @@ -1,5 +1,19 @@ + + + + +
+ + {{ canto()?.id?.startsWith('my_') ? 'M' : canto()?.id_canti }} + + + {{ getCommunitySongNumber(canto()) }} + {{ canto()?.titolo || 'Player' }} + +
+
@@ -12,21 +26,6 @@ - -
- - {{ canto()?.id?.startsWith('my_') ? 'M' : canto()?.id_canti }} - - - {{ getCommunitySongNumber(canto()) }} - {{ canto()?.titolo || 'Player' }} - - Non Validato -
-
- - - @@ -44,50 +43,19 @@
- -
- - + +
+ + -
- - V{{ autoscrollSpeed() }} -
- - + + + + +
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/app/pages/player/player.page.scss b/src/app/pages/player/player.page.scss index 5b35966..62dcfe2 100644 --- a/src/app/pages/player/player.page.scss +++ b/src/app/pages/player/player.page.scss @@ -15,7 +15,8 @@ // Force left alignment in Ionic toolbar ion-title { - padding-inline: 8px; + padding-inline-start: 56px; // Clear the back button on iOS/Apple devices + padding-inline-end: 8px; text-align: left !important; } @@ -553,3 +554,9 @@ ion-content.full-screen-content { color: #c0392b !important; border-color: #c0392b !important; } + +:host-context(.md) { + .top-toolbar ion-title { + padding-inline-start: 8px; + } +} diff --git a/src/app/pages/player/player.page.ts b/src/app/pages/player/player.page.ts index ab40444..9f7c1c2 100644 --- a/src/app/pages/player/player.page.ts +++ b/src/app/pages/player/player.page.ts @@ -69,7 +69,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } // Apply transposition if in chords mode - if (this.showChords() && this.transposeAmount() !== 0) { + if (this.showChords()) { return this.lyricsParser.transposeSections(sections, this.transposeAmount()); } @@ -121,6 +121,12 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { private songStartTime: number = 0; constructor() { + // Sync transposition automatically to display/projection page + effect(() => { + const amount = this.transposeAmount(); + this.channel.postMessage({ type: 'SYNC_TRANSPOSE', amount }); + }); + // Sync mode from global settings effect(() => { this.showChords.set(this.settingsService.showChordsDefault()); @@ -186,23 +192,52 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { this.songStartTime = Date.now(); this.cantiService.getStorage()?.set('last_song_id', id); this.channel.postMessage({ type: 'SYNC_CANTO', id: found.id }); - - // Set custom community transposition if active - if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) { - const settings = this.comunitaService.comunitaCantiSettings(); - const songSetting = settings.find(s => s.id_canti === found.id_canti || s.id_canti === Number(found.id)); - if (songSetting && songSetting.tonalita !== undefined) { - this.transposeAmount.set(songSetting.tonalita); - } else { - this.transposeAmount.set(0); - } - } else { - this.transposeAmount.set(0); - } } } } }, { allowSignalWrites: true }); + + // Reactive transposition and speed determination based on canto, playlists, and community settings + effect(() => { + const c = this.canto(); + if (!c) return; + + const activePlaylistId = this.playlistService.activePlaylistId(); + let playlistSongSetting: any = null; + if (activePlaylistId) { + const pl = this.playlistService.playlists().find(p => p.id === activePlaylistId); + if (pl && pl.songSettings && pl.songSettings[c.id]) { + playlistSongSetting = pl.songSettings[c.id]; + } + } + + if (playlistSongSetting) { + this.transposeAmount.set(playlistSongSetting.tonalita !== undefined ? playlistSongSetting.tonalita : 0); + this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2); + } else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) { + const settings = this.comunitaService.comunitaCantiSettings(); + const songSetting = settings.find(s => s.id_canti === c.id_canti || s.id_canti === Number(c.id)); + if (songSetting) { + if (songSetting.tonalita !== undefined) { + this.transposeAmount.set(songSetting.tonalita); + } else { + this.transposeAmount.set(0); + } + if (songSetting.speed !== undefined && songSetting.speed > 0) { + const mappedSpeed = Math.max(1, Math.min(10, Math.round(songSetting.speed / 100))); + this.autoscrollSpeed.set(mappedSpeed); + } else { + this.autoscrollSpeed.set(2); + } + } else { + this.transposeAmount.set(0); + this.autoscrollSpeed.set(2); + } + } else { + this.transposeAmount.set(0); + this.autoscrollSpeed.set(2); + } + }, { allowSignalWrites: true }); } ngAfterViewInit() { @@ -304,21 +339,31 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { transposeUp() { this.transposeAmount.update(v => v + 1); - this.channel.postMessage({ type: 'SYNC_TRANSPOSE', amount: this.transposeAmount() }); const c = this.canto(); if (c) { - this.statsService.updateSongSettings(c.id_canti, this.transposeAmount()); + const dbSpeed = this.autoscrollSpeed() * 100; + this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed); + + const activePlaylistId = this.playlistService.activePlaylistId(); + if (activePlaylistId) { + this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed()); + } } } transposeDown() { this.transposeAmount.update(v => v - 1); - this.channel.postMessage({ type: 'SYNC_TRANSPOSE', amount: this.transposeAmount() }); const c = this.canto(); if (c) { - this.statsService.updateSongSettings(c.id_canti, this.transposeAmount()); + const dbSpeed = this.autoscrollSpeed() * 100; + this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed); + + const activePlaylistId = this.playlistService.activePlaylistId(); + if (activePlaylistId) { + this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed()); + } } } @@ -600,10 +645,30 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { increaseAutoscrollSpeed() { this.autoscrollSpeed.update(s => Math.min(10, s + 1)); + const c = this.canto(); + if (c) { + const dbSpeed = this.autoscrollSpeed() * 100; + this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed); + + const activePlaylistId = this.playlistService.activePlaylistId(); + if (activePlaylistId) { + this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed()); + } + } } decreaseAutoscrollSpeed() { this.autoscrollSpeed.update(s => Math.max(1, s - 1)); + const c = this.canto(); + if (c) { + const dbSpeed = this.autoscrollSpeed() * 100; + this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed); + + const activePlaylistId = this.playlistService.activePlaylistId(); + if (activePlaylistId) { + this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed()); + } + } } private logPreviousSongTime() { diff --git a/src/app/pages/playlist/playlist.page.ts b/src/app/pages/playlist/playlist.page.ts index 5ae4292..02ae6f3 100644 --- a/src/app/pages/playlist/playlist.page.ts +++ b/src/app/pages/playlist/playlist.page.ts @@ -60,6 +60,15 @@ export class PlaylistPage { moveItemInArray(this.localSongs, event.previousIndex, event.currentIndex); } + getPlaylistSongSettings(): any { + const activeId = this.playlistService.activePlaylistId(); + if (activeId) { + const pl = this.playlistService.playlists().find(p => p.id === activeId); + return pl ? pl.songSettings : undefined; + } + return undefined; + } + async savePlaylist() { const alert = await this.alertCtrl.create({ header: 'Salva Playlist', @@ -118,7 +127,8 @@ export class PlaylistPage { this.savedPlaylistName = data.name; const ids = this.localSongs.map(s => s.id); await this.playlistService.savePlaylist(data.name, ids); - this.qrCodeImage = await this.playlistService.generateQR(ids, data.name); + const songSettings = this.getPlaylistSongSettings(); + this.qrCodeImage = await this.playlistService.generateQR(ids, data.name, songSettings); this.showToast('Playlist salvata!'); return true; } @@ -135,7 +145,8 @@ export class PlaylistPage { const ids = this.localSongs.map(s => s.id); const name = this.savedPlaylistName || 'Playlist Condivisa'; - this.qrCodeImage = await this.playlistService.generateQR(ids, name); + const songSettings = this.getPlaylistSongSettings(); + this.qrCodeImage = await this.playlistService.generateQR(ids, name, songSettings); } downloadQR() { @@ -157,7 +168,8 @@ export class PlaylistPage { async shareQR() { if (!this.qrCodeImage) return; const name = this.savedPlaylistName || 'playlist'; - const shareLink = this.playlistService.getShareLink(this.localSongs.map(s => s.id), name); + const songSettings = this.getPlaylistSongSettings(); + const shareLink = this.playlistService.getShareLink(this.localSongs.map(s => s.id), name, songSettings); const fileName = `${name.toLowerCase().replace(/\s+/g, '_')}_qr.png`; try { @@ -180,6 +192,13 @@ export class PlaylistPage { text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla: ${shareLink}` }); } else { + // Copy to clipboard AND download QR! + try { + if (navigator.clipboard) { + await navigator.clipboard.writeText(shareLink); + this.showToast('Link copiato negli appunti! QR scaricato.'); + } + } catch(e) {} this.downloadQR(); } } diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index 65689bf..818607f 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -35,11 +35,11 @@ -
-

+

+

Apple non consente l'installazione automatica dei siti web. Segui questi semplici passi da Safari:

-
    +
    1. Tocca il pulsante di Condivisione nella barra di navigazione inferiore di Safari.
    2. @@ -185,6 +185,30 @@
+ +
+
+

+ Notazione Accordi Preferita +

+
+ +
+
+
+ Diesis (#) +
+
+ Bemolle (b) +
+
+
+
+ @@ -207,6 +231,15 @@ + + + + +

Verifica Aggiornamenti App

+

Forza la ricerca di una nuova versione dell'applicazione

+
+
+

Versione: v{{ version }} • diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts index 45a560e..a3a2cc5 100644 --- a/src/app/pages/settings/settings.page.ts +++ b/src/app/pages/settings/settings.page.ts @@ -3,11 +3,12 @@ import { ThemeService } from '../../services/theme.service'; import { SettingsService } from '../../services/settings.service'; import { CantiService } from '../../services/canti.service'; import { ConnectivityService } from '../../services/connectivity.service'; -import { SwUpdate } from '@angular/service-worker'; +import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { ToastController, ModalController, AlertController } from '@ionic/angular'; import { VERSION } from '../../version'; import { PlaylistService } from '../../services/playlist.service'; import { Router } from '@angular/router'; +import { filter, first } from 'rxjs/operators'; import { MyCantiService } from '../../services/my-canti.service'; import { CantiLettureService } from '../../services/canti-letture.service'; @@ -91,9 +92,23 @@ export class SettingsPage { }); await toast.present(); - setTimeout(() => { - window.location.reload(); - }, 2000); + this.swUpdate.versionUpdates + .pipe( + filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), + first() + ) + .subscribe(async () => { + console.log('[PWA-Update] FullRefresh: version ready, activating...'); + await this.swUpdate.activateUpdate(); + window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now()); + }); + + setTimeout(async () => { + try { + await this.swUpdate.activateUpdate(); + } catch(e) {} + window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now()); + }, 6000); return; } } catch (err) { @@ -108,4 +123,70 @@ export class SettingsPage { }); await toast.present(); } + + async checkForAppUpdate() { + if (!this.swUpdate.isEnabled) { + const toast = await this.toastCtrl.create({ + message: 'Aggiornamenti non supportati su questo browser.', + duration: 3000, + color: 'medium' + }); + await toast.present(); + return; + } + + const toastLoading = await this.toastCtrl.create({ + message: 'Ricerca aggiornamenti in corso...', + duration: 1500, + color: 'secondary' + }); + await toastLoading.present(); + + try { + const updateFound = await this.swUpdate.checkForUpdate(); + if (updateFound) { + const toast = await this.toastCtrl.create({ + message: 'Nuova versione trovata! Installazione e attivazione in corso...', + duration: 3000, + color: 'success' + }); + await toast.present(); + + // Sottoscrizione per attivare l'aggiornamento appena terminato il download + this.swUpdate.versionUpdates + .pipe( + filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), + first() + ) + .subscribe(async () => { + console.log('[PWA-Update] Manual check: version ready, activating...'); + await this.swUpdate.activateUpdate(); + window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now()); + }); + + // Timeout di sicurezza per forzare l'attivazione e il ricaricamento se è già scaricato + setTimeout(async () => { + try { + await this.swUpdate.activateUpdate(); + } catch(e) {} + window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now()); + }, 8000); + } else { + const toast = await this.toastCtrl.create({ + message: 'L\'applicazione è già aggiornata all\'ultima versione.', + duration: 3000, + color: 'success' + }); + await toast.present(); + } + } catch (err) { + console.error('Check update failed', err); + const toast = await this.toastCtrl.create({ + message: 'Errore durante la ricerca di aggiornamenti.', + duration: 3000, + color: 'danger' + }); + await toast.present(); + } + } } diff --git a/src/app/services/canti.service.ts b/src/app/services/canti.service.ts index 66c8128..5663d83 100644 --- a/src/app/services/canti.service.ts +++ b/src/app/services/canti.service.ts @@ -13,6 +13,7 @@ export interface Canto { id_momenti?: number[]; data_update?: string; nonValidato?: boolean; + isPersonal?: boolean; } export interface Indice { diff --git a/src/app/services/comunita.service.ts b/src/app/services/comunita.service.ts index f35f2fa..23baeee 100644 --- a/src/app/services/comunita.service.ts +++ b/src/app/services/comunita.service.ts @@ -186,7 +186,8 @@ export class ComunitaService { link_youtube: cp.link_youtube || '', id_momenti: [], data_update: cp.data_update || '', - nonValidato: true + nonValidato: Number(cp.stato) === 10, + isPersonal: true })); this.comunitaCode.set(trimmedCode); diff --git a/src/app/services/lyrics-parser.service.ts b/src/app/services/lyrics-parser.service.ts index d3ebc6d..5a930ce 100644 --- a/src/app/services/lyrics-parser.service.ts +++ b/src/app/services/lyrics-parser.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; +import { SettingsService } from './settings.service'; export interface ChordSegment { text: string; @@ -20,6 +21,7 @@ export interface ParsedSection { providedIn: 'root' }) export class LyricsParserService { + private settingsService = inject(SettingsService); /** * Parse plain text (campo 'testo') into structured sections. @@ -180,7 +182,7 @@ export class LyricsParserService { * Handles Italian notation. */ transposeChord(chord: string, semitones: number): string { - if (!chord || semitones === 0) return chord; + if (!chord) return chord; // Handle slash chords (e.g., DO/SOL) if (chord.includes('/')) { @@ -193,8 +195,9 @@ export class LyricsParserService { let root = ''; let suffix = ''; + const upperChord = chord.toUpperCase(); for (const r of possibleRoots) { - if (chord.startsWith(r)) { + if (upperChord.startsWith(r)) { root = r; suffix = chord.substring(r.length); break; @@ -210,8 +213,24 @@ export class LyricsParserService { let newIndex = (index + semitones) % 12; if (newIndex < 0) newIndex += 12; - // Preserve the original notation style (sharp or flat) if possible - const useFlat = this.flatScale.includes(root); + // Decide flat vs sharp notation based on SettingsService preference: + const pref = this.settingsService.chordNotationPreference(); + let useFlat = false; + if (pref === 'diesis') { + useFlat = false; + } else if (pref === 'bemolle') { + useFlat = true; + } else { + // Fallback/Default logic + if (root.includes('#')) { + useFlat = false; + } else if (root.toLowerCase().includes('b')) { + useFlat = true; + } else { + useFlat = semitones < 0; + } + } + const newRoot = useFlat ? this.flatScale[newIndex] : this.scale[newIndex]; return newRoot + suffix; @@ -221,8 +240,6 @@ export class LyricsParserService { * Transpose all chords in a parsed structure. */ transposeSections(sections: ParsedSection[], semitones: number): ParsedSection[] { - if (semitones === 0) return sections; - return sections.map(section => ({ ...section, lines: section.lines.map(line => ({ diff --git a/src/app/services/playlist.service.ts b/src/app/services/playlist.service.ts index 42729d4..bf93d69 100644 --- a/src/app/services/playlist.service.ts +++ b/src/app/services/playlist.service.ts @@ -3,6 +3,7 @@ import { Storage } from '@ionic/storage-angular'; import { Canto, CantiService } from './canti.service'; import { ComunitaService } from './comunita.service'; import * as QRCode from 'qrcode'; +import { ToastController } from '@ionic/angular'; @Injectable({ providedIn: 'root' @@ -11,6 +12,7 @@ export class PlaylistService { private storage = inject(Storage); private cantiService = inject(CantiService); private comunitaService = inject(ComunitaService); + private toastCtrl = inject(ToastController); public selectionMode = signal(false); public selectedIds = signal>(new Set()); @@ -23,6 +25,7 @@ export class PlaylistService { public activePlaylistId = signal(null); private _storage: Storage | null = null; + private initPromise!: Promise; // Community scalette exposed as playlists (only when community filter is active) public comunitaPlaylists = computed(() => { @@ -49,7 +52,7 @@ export class PlaylistService { }); constructor() { - this.init(); + this.initPromise = this.init(); // Watch for context changes to dynamically reload the correct playlists effect(() => { @@ -114,7 +117,8 @@ export class PlaylistService { }); } - async savePlaylist(name: string, ids: string[]) { + async savePlaylist(name: string, ids: string[], songSettings?: any) { + await this.initPromise; const key = this.getPlaylistsStorageKey(); const lastKey = `lastPlaylist_${key}`; const editId = this.activePlaylistId(); @@ -123,7 +127,8 @@ export class PlaylistService { if (editId) { this.playlists.update(p => p.map(pl => { if (pl.id === editId) { - return { ...pl, name, ids }; + const mergedSettings = songSettings || pl.songSettings || {}; + return { ...pl, name, ids, songSettings: mergedSettings }; } return pl; })); @@ -133,6 +138,7 @@ export class PlaylistService { id: Date.now().toString(), name, ids, + songSettings: songSettings || {}, createdAt: new Date() }; this.playlists.update(p => [newPlaylist, ...p]); @@ -150,14 +156,39 @@ export class PlaylistService { this.activePlaylistId.set(newPlaylist.id); } + async updatePlaylistSongSettings(playlistId: string, songId: string, tonalita: number, speed: number) { + await this.initPromise; + this.playlists.update(p => p.map(pl => { + if (pl.id === playlistId) { + const songSettings = { ...(pl.songSettings || {}) }; + songSettings[songId] = { tonalita, speed }; + return { ...pl, songSettings }; + } + return pl; + })); + + const key = this.getPlaylistsStorageKey(); + await this._storage?.set(key, this.playlists()); + + // Also update lastPlaylist if it is the current one + const lastKey = `lastPlaylist_${key}`; + const last = this.lastPlaylist(); + if (last && last.id === playlistId) { + const updatedLast = this.playlists().find(pl => pl.id === playlistId); + this.lastPlaylist.set(updatedLast || null); + await this._storage?.set(lastKey, updatedLast); + } + } + async deletePlaylist(id: string) { + await this.initPromise; const key = this.getPlaylistsStorageKey(); this.playlists.update(p => p.filter(pl => pl.id !== id)); await this._storage?.set(key, this.playlists()); } - async generateQR(ids: string[], name: string): Promise { - const data = this.getShareLink(ids, name); + async generateQR(ids: string[], name: string, songSettings?: any): Promise { + const data = this.getShareLink(ids, name, songSettings); return await QRCode.toDataURL(data, { width: 400, margin: 2, @@ -168,8 +199,48 @@ export class PlaylistService { }); } - getShareLink(ids: string[], name: string): string { - const data = JSON.stringify({ name, ids }); + getShareLink(ids: string[], name: string, songSettings?: any): string { + const mergedSettings = { ...(songSettings || {}) }; + const cc = this.comunitaService.comunitaCode(); + const isCommunityActive = this.comunitaService.isFilterActive(); + + if (cc && isCommunityActive) { + const communitySettings = this.comunitaService.comunitaCantiSettings(); + for (const id of ids) { + if (mergedSettings[id] === undefined) { + const cSettings = communitySettings.find(s => + s.id_canti === Number(id) || String(s.id_canti) === id + ); + if (cSettings) { + const tonalita = cSettings.tonalita !== undefined ? cSettings.tonalita : 0; + let speed = 2; + if (cSettings.speed !== undefined && cSettings.speed > 0) { + speed = Math.max(1, Math.min(10, Math.round(cSettings.speed / 100))); + } + mergedSettings[id] = { tonalita, speed }; + } + } + } + } + + const shareObj: any = { name, ids, songSettings: mergedSettings }; + + if (cc && isCommunityActive) { + const communityCantiIds = this.comunitaService.comunitaCantiIds(); + const communityCantiPersonali = this.comunitaService.comunitaCantiPersonali(); + + const containsCommunitySong = ids.some(id => { + const isStandard = communityCantiIds.includes(id) || communityCantiIds.includes(Number(id)); + const isPersonal = communityCantiPersonali.some(cp => cp.id === id); + return isStandard || isPersonal; + }); + + if (containsCommunitySong) { + shareObj.comunitaCode = cc; + } + } + + const data = JSON.stringify(shareObj); // Use btoa safely for UTF-8 strings const base64 = btoa(unescape(encodeURIComponent(data))); @@ -180,16 +251,19 @@ export class PlaylistService { processImportJson(json: any): boolean { if (json && json.name && json.ids) { + this.activePlaylistId.set(null); // Forza il salvataggio come nuova playlist indipendente ed editabile this.activeListIds.set(json.ids); this.activeListName.set(json.name); + // Automatically save to the device! + this.savePlaylist(json.name, json.ids, json.songSettings); return true; } return false; } - async sharePlaylistQR(ids: string[], name: string) { - const qrImage = await this.generateQR(ids, name); - const shareLink = this.getShareLink(ids, name); + async sharePlaylistQR(ids: string[], name: string, songSettings?: any) { + const qrImage = await this.generateQR(ids, name, songSettings); + const shareLink = this.getShareLink(ids, name, songSettings); const fileName = `${name.toLowerCase().replace(/\s+/g, '_')}_qr.png`; try { @@ -204,7 +278,21 @@ export class PlaylistService { text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla subito: ${shareLink}` }); } else { - // Fallback: download + // 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); + } + const link = document.createElement('a'); link.href = qrImage; link.download = fileName; diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 15d19c6..a990ae2 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -47,6 +47,9 @@ export class SettingsService { /** Attiva autoscroll acustico nel dettaglio canto: true = attivo */ public enableAcousticAutoscroll = signal(false); + /** Preferenza notazione accordi: diesis o bemolle */ + public chordNotationPreference = signal<'diesis' | 'bemolle'>('diesis'); + private wakeLock: any = null; // PWA installation signals @@ -103,6 +106,7 @@ export class SettingsService { localStorage.setItem('show-update-date', 'true'); localStorage.setItem('enable-standard-autoscroll', 'true'); localStorage.setItem('enable-acoustic-autoscroll', 'false'); + localStorage.setItem('chord-notation-preference', 'diesis'); // ThemeService high contrast default localStorage.setItem('high-contrast', 'true'); @@ -187,6 +191,13 @@ export class SettingsService { this.enableAcousticAutoscroll.set(false); } + const savedNotation = localStorage.getItem('chord-notation-preference'); + if (savedNotation !== null) { + this.chordNotationPreference.set(savedNotation === 'bemolle' ? 'bemolle' : 'diesis'); + } else { + this.chordNotationPreference.set('diesis'); + } + // Sync browser fullscreen state with listeners (supporting vendor prefixes) const updateFullscreenState = () => { const isFs = !!( @@ -255,6 +266,10 @@ export class SettingsService { localStorage.setItem('enable-acoustic-autoscroll', this.enableAcousticAutoscroll().toString()); }); + effect(() => { + localStorage.setItem('chord-notation-preference', this.chordNotationPreference()); + }); + effect(() => { const active = this.keepScreenOn(); localStorage.setItem('keep-screen-on', active.toString()); @@ -381,6 +396,10 @@ export class SettingsService { localStorage.setItem('enable-acoustic-autoscroll', newValue.toString()); } + setChordNotationPreference(val: 'diesis' | 'bemolle') { + this.chordNotationPreference.set(val); + } + async installPwa() { const promptEvent = this.deferredPrompt(); if (!promptEvent) { diff --git a/src/app/version.ts b/src/app/version.ts index b7db90d..007f071 100644 --- a/src/app/version.ts +++ b/src/app/version.ts @@ -1 +1 @@ -export const VERSION = '2026.05.22.0110'; +export const VERSION = '2026.05.23.1602';