import { Injectable, signal, effect } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class SettingsService { /** Default mode for viewing songs: true = chords, false = text only */ public showChordsDefault = signal(true); /** UI Fullscreen mode (lyrics only): true = active */ public fullscreenMode = signal(false); /** Browser Fullscreen state: true = fullscreen active */ public browserFullscreen = signal( typeof document !== 'undefined' && !!( document.fullscreenElement || (document as any).webkitFullscreenElement || (document as any).mozFullScreenElement || (document as any).msFullscreenElement ) ); /** Avanzamento automatico: true = passa al brano successivo automaticamente */ public autoAdvance = signal(true); /** Editor e Canti Personali: true = mostra funzioni aggiunta e lista "Miei" */ public showEditor = signal(false); /** Schermo sempre acceso: true = attiva Screen Wake Lock */ public keepScreenOn = signal(true); /** Funzionalità Comunità: true = il chip comunità è visibile nella home */ public comunitaEnabled = signal(false); /** Invio dati statistici: true = invia pacchetto dati statistici */ public invioDatiStatistici = signal(false); /** Visualizza tag sotto autore nella lista canti: true = attivo */ public showTagsInList = signal(true); /** Visualizza data update sotto autore nella lista canti: true = attivo */ public showUpdateDate = signal(true); /** Visualizza durata, bpm e tonalità sotto autore / titolo: true = attivo */ public showDurationBpmTonality = signal(true); /** Attiva autoscroll standard nel dettaglio canto: true = attivo */ public enableStandardAutoscroll = signal(false); /** Attiva autoscroll visuale nel dettaglio canto: true = attivo */ public enableVisualAutoscroll = signal(true); /** Preferenza notazione accordi: diesis o bemolle */ public chordNotationPreference = signal<'diesis' | 'bemolle'>('diesis'); /** Avanzamento a pagine del karaoke manuale: true = i tasti next/prev voltano la pagina */ public karaokePageScrollMode = signal(false); /** Vista orizzontale per proiezione: true = attiva layout landscape per proiezione */ public landscapeProjectionEnabled = signal(false); /** Schermo nero per l'ascolto delle playlist in macchina: true = attivo */ public carModeBlackScreen = signal(false); /** Global zoom/font size factor for song presentation */ public globalZoom = signal(1.0); /** Identificativo utente univoco per la gestione delle comunità */ public userUuid = signal(''); /** Identificativo originario assegnato alla prima installazione */ public originalUserUuid = signal(''); /** Nome associato all'identità utente */ public userName = signal(''); private wakeLock: any = null; // PWA installation signals public deferredPrompt = signal(null); public showInstallButton = signal(false); public isStandalone = signal(false); public isIos = signal(false); public isAndroid = signal(false); public isVersionCheckComplete = signal(false); constructor() { // Gestione/Generazione ID utente univoco let savedUuid = localStorage.getItem('user-uuid'); if (!savedUuid) { if (typeof crypto !== 'undefined' && crypto.randomUUID) { savedUuid = crypto.randomUUID(); } else { savedUuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } localStorage.setItem('user-uuid', savedUuid); } this.userUuid.set(savedUuid); // Salvataggio dell'ID originario (alla prima installazione) se non è già presente let originalUuid = localStorage.getItem('original-user-uuid'); if (!originalUuid) { originalUuid = savedUuid; localStorage.setItem('original-user-uuid', originalUuid); } this.originalUserUuid.set(originalUuid); const savedName = localStorage.getItem('user-name'); if (savedName) { this.userName.set(savedName); } // Detect PWA status try { this.isStandalone.set( window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone === true ); this.isIos.set( /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream ); this.isAndroid.set( /Android/i.test(navigator.userAgent) ); } catch (e) { console.warn('Browser environment does not support display-mode media query'); } window.addEventListener('beforeinstallprompt', (e: any) => { // Prevent the mini-infobar from appearing on mobile e.preventDefault(); // Stash the event so it can be triggered later. this.deferredPrompt.set(e); // Update UI notify the user they can install the PWA this.showInstallButton.set(true); }); window.addEventListener('appinstalled', () => { this.deferredPrompt.set(null); this.showInstallButton.set(false); this.isStandalone.set(true); console.log('PWA was installed'); }); // Migration: force settings defaults once for existing users to match the new rules const migrationKey = 'defaults-migrated-20260605'; if (localStorage.getItem(migrationKey) !== 'true') { localStorage.setItem('show-chords-default', 'true'); localStorage.setItem('fullscreen-mode', this.isIos().toString()); localStorage.setItem('show-editor', 'false'); localStorage.setItem('auto-advance', 'true'); localStorage.setItem('keep-screen-on', 'true'); localStorage.setItem('comunita-enabled', 'false'); localStorage.setItem('invio-dati-statistici', 'false'); localStorage.setItem('show-tags-in-list', 'true'); localStorage.setItem('show-update-date', 'true'); localStorage.setItem('show-duration-bpm-tonality', 'true'); localStorage.setItem('enable-standard-autoscroll', 'false'); localStorage.setItem('enable-visual-autoscroll', 'true'); localStorage.setItem('chord-notation-preference', 'diesis'); // ThemeService high contrast default localStorage.setItem('high-contrast', 'true'); localStorage.setItem(migrationKey, 'true'); } const savedChords = localStorage.getItem('show-chords-default'); if (savedChords !== null) { this.showChordsDefault.set(savedChords === 'true'); } else { this.showChordsDefault.set(true); } const savedFullscreen = localStorage.getItem('fullscreen-mode'); if (savedFullscreen !== null) { this.fullscreenMode.set(savedFullscreen === 'true'); } else { this.fullscreenMode.set(this.isIos()); } const savedEditor = localStorage.getItem('show-editor'); if (savedEditor !== null) { this.showEditor.set(savedEditor === 'true'); } else { this.showEditor.set(false); } const savedAutoAdvance = localStorage.getItem('auto-advance'); if (savedAutoAdvance !== null) { this.autoAdvance.set(savedAutoAdvance === 'true'); } else { this.autoAdvance.set(true); } // Keep screen always on by default and always active this.keepScreenOn.set(true); const savedComunitaEnabled = localStorage.getItem('comunita-enabled'); if (savedComunitaEnabled !== null) { this.comunitaEnabled.set(savedComunitaEnabled === 'true'); } else { this.comunitaEnabled.set(false); } const savedStats = localStorage.getItem('invio-dati-statistici'); if (savedStats !== null) { this.invioDatiStatistici.set(savedStats === 'true'); } else { this.invioDatiStatistici.set(false); } const savedShowTags = localStorage.getItem('show-tags-in-list'); if (savedShowTags !== null) { this.showTagsInList.set(savedShowTags === 'true'); } else { this.showTagsInList.set(true); } const savedShowUpdateDate = localStorage.getItem('show-update-date'); if (savedShowUpdateDate !== null) { this.showUpdateDate.set(savedShowUpdateDate === 'true'); } else { this.showUpdateDate.set(true); } const savedShowDurBpmTon = localStorage.getItem('show-duration-bpm-tonality'); if (savedShowDurBpmTon !== null) { this.showDurationBpmTonality.set(savedShowDurBpmTon === 'true'); } else { this.showDurationBpmTonality.set(true); } const savedStandardAutoscroll = localStorage.getItem('enable-standard-autoscroll'); if (savedStandardAutoscroll !== null) { this.enableStandardAutoscroll.set(savedStandardAutoscroll === 'true'); } else { this.enableStandardAutoscroll.set(false); } const savedVisualAutoscroll = localStorage.getItem('enable-visual-autoscroll'); if (savedVisualAutoscroll !== null) { this.enableVisualAutoscroll.set(savedVisualAutoscroll === 'true'); } else { this.enableVisualAutoscroll.set(true); } const savedNotation = localStorage.getItem('chord-notation-preference'); if (savedNotation !== null) { this.chordNotationPreference.set(savedNotation === 'bemolle' ? 'bemolle' : 'diesis'); } else { this.chordNotationPreference.set('diesis'); } const savedKaraokePageScrollMode = localStorage.getItem('karaoke-page-scroll-mode'); if (savedKaraokePageScrollMode !== null) { this.karaokePageScrollMode.set(savedKaraokePageScrollMode === 'true'); } else { this.karaokePageScrollMode.set(false); } const savedLandscapeProjectionEnabled = localStorage.getItem('landscape-projection-enabled'); if (savedLandscapeProjectionEnabled !== null) { this.landscapeProjectionEnabled.set(savedLandscapeProjectionEnabled === 'true'); } else { this.landscapeProjectionEnabled.set(false); } const savedCarModeBlackScreen = localStorage.getItem('car-mode-black-screen'); if (savedCarModeBlackScreen !== null) { this.carModeBlackScreen.set(savedCarModeBlackScreen === 'true'); } else { this.carModeBlackScreen.set(false); } const savedGlobalZoom = localStorage.getItem('global-zoom'); if (savedGlobalZoom !== null) { const parsed = parseFloat(savedGlobalZoom); this.globalZoom.set(isNaN(parsed) ? 1.0 : parsed); } else { this.globalZoom.set(1.0); } // Sync browser fullscreen state with listeners (supporting vendor prefixes) const updateFullscreenState = () => { const isFs = !!( document.fullscreenElement || (document as any).webkitFullscreenElement || (document as any).mozFullScreenElement || (document as any).msFullscreenElement ); this.browserFullscreen.set(isFs); }; document.addEventListener('fullscreenchange', updateFullscreenState); document.addEventListener('webkitfullscreenchange', updateFullscreenState); document.addEventListener('mozfullscreenchange', updateFullscreenState); document.addEventListener('MSFullscreenChange', updateFullscreenState); effect(() => { localStorage.setItem('show-chords-default', this.showChordsDefault().toString()); }); effect(() => { const mode = this.fullscreenMode(); localStorage.setItem('fullscreen-mode', mode.toString()); }); effect(() => { localStorage.setItem('auto-advance', this.autoAdvance().toString()); }); effect(() => { localStorage.setItem('show-tags-in-list', this.showTagsInList().toString()); }); effect(() => { localStorage.setItem('show-update-date', this.showUpdateDate().toString()); }); effect(() => { localStorage.setItem('show-duration-bpm-tonality', this.showDurationBpmTonality().toString()); }); effect(() => { localStorage.setItem('enable-standard-autoscroll', this.enableStandardAutoscroll().toString()); }); effect(() => { localStorage.setItem('enable-visual-autoscroll', this.enableVisualAutoscroll().toString()); }); effect(() => { localStorage.setItem('chord-notation-preference', this.chordNotationPreference()); }); effect(() => { localStorage.setItem('karaoke-page-scroll-mode', this.karaokePageScrollMode().toString()); }); effect(() => { localStorage.setItem('landscape-projection-enabled', this.landscapeProjectionEnabled().toString()); }); effect(() => { localStorage.setItem('car-mode-black-screen', this.carModeBlackScreen().toString()); }); effect(() => { localStorage.setItem('global-zoom', this.globalZoom().toString()); }); effect(() => { const active = this.keepScreenOn(); localStorage.setItem('keep-screen-on', active.toString()); if (active) { this.requestWakeLock(); } else { this.releaseWakeLock(); } }); // Re-request on visibility change document.addEventListener('visibilitychange', () => { if (this.keepScreenOn() && document.visibilityState === 'visible') { this.requestWakeLock(); } }); } setShowChordsDefault(val: boolean) { this.showChordsDefault.set(val); } toggleFullscreenMode() { this.fullscreenMode.update(v => !v); } toggleAutoAdvance() { this.autoAdvance.update(v => !v); } toggleEditor() { const newValue = !this.showEditor(); this.showEditor.set(newValue); localStorage.setItem('show-editor', newValue.toString()); } toggleComunitaEnabled() { const newValue = !this.comunitaEnabled(); this.comunitaEnabled.set(newValue); localStorage.setItem('comunita-enabled', newValue.toString()); } private async requestWakeLock() { if ('wakeLock' in navigator) { try { this.wakeLock = await (navigator as any).wakeLock.request('screen'); } catch (err: any) { console.error(`Wake Lock error: ${err.name}, ${err.message}`); } } } private releaseWakeLock() { if (this.wakeLock) { this.wakeLock.release(); this.wakeLock = null; } } toggleBrowserFullscreen() { const isFs = !!( document.fullscreenElement || (document as any).webkitFullscreenElement || (document as any).mozFullScreenElement || (document as any).msFullscreenElement ); if (!isFs) { const docEl = document.documentElement as any; if (docEl.requestFullscreen) { docEl.requestFullscreen().catch((err: any) => console.error(err)); } else if (docEl.webkitRequestFullscreen) { docEl.webkitRequestFullscreen(); } else if (docEl.mozRequestFullScreen) { docEl.mozRequestFullScreen(); } else if (docEl.msRequestFullscreen) { docEl.msRequestFullscreen(); } } else { const doc = document as any; if (doc.exitFullscreen) { doc.exitFullscreen().catch((err: any) => console.error(err)); } else if (doc.webkitExitFullscreen) { doc.webkitExitFullscreen(); } else if (doc.mozCancelFullScreen) { doc.mozCancelFullScreen(); } else if (doc.msExitFullscreen) { doc.msExitFullscreen(); } } } toggleInvioDatiStatistici() { const newValue = !this.invioDatiStatistici(); this.invioDatiStatistici.set(newValue); localStorage.setItem('invio-dati-statistici', newValue.toString()); } toggleShowTagsInList() { const newValue = !this.showTagsInList(); this.showTagsInList.set(newValue); localStorage.setItem('show-tags-in-list', newValue.toString()); } toggleShowUpdateDate() { const newValue = !this.showUpdateDate(); this.showUpdateDate.set(newValue); localStorage.setItem('show-update-date', newValue.toString()); } toggleShowDurationBpmTonality() { const newValue = !this.showDurationBpmTonality(); this.showDurationBpmTonality.set(newValue); localStorage.setItem('show-duration-bpm-tonality', newValue.toString()); } toggleStandardAutoscroll() { const newValue = !this.enableStandardAutoscroll(); this.enableStandardAutoscroll.set(newValue); localStorage.setItem('enable-standard-autoscroll', newValue.toString()); } toggleVisualAutoscroll() { const newValue = !this.enableVisualAutoscroll(); this.enableVisualAutoscroll.set(newValue); localStorage.setItem('enable-visual-autoscroll', newValue.toString()); } toggleKaraokePageScrollMode() { const newValue = !this.karaokePageScrollMode(); this.karaokePageScrollMode.set(newValue); localStorage.setItem('karaoke-page-scroll-mode', newValue.toString()); } toggleLandscapeProjectionEnabled() { const newValue = !this.landscapeProjectionEnabled(); this.landscapeProjectionEnabled.set(newValue); localStorage.setItem('landscape-projection-enabled', newValue.toString()); } toggleCarModeBlackScreen() { const newValue = !this.carModeBlackScreen(); this.carModeBlackScreen.set(newValue); localStorage.setItem('car-mode-black-screen', newValue.toString()); } setChordNotationPreference(val: 'diesis' | 'bemolle') { this.chordNotationPreference.set(val); } setUserUuid(uuid: string) { const trimmed = uuid.trim(); if (trimmed) { this.userUuid.set(trimmed); localStorage.setItem('user-uuid', trimmed); } } setUserName(name: string) { const trimmed = name.trim(); this.userName.set(trimmed); localStorage.setItem('user-name', trimmed); } async installPwa(): Promise { const promptEvent = this.deferredPrompt(); if (!promptEvent) { return undefined; } // Show the install prompt promptEvent.prompt(); // Wait for the user to respond to the prompt const { outcome } = await promptEvent.userChoice; console.log(`User response to the install prompt: ${outcome}`); // We've used the prompt, and can't use it again, discard it this.deferredPrompt.set(null); this.showInstallButton.set(false); return outcome; } }