gestione comunità
This commit is contained in:
@@ -43,6 +43,27 @@
|
||||
{{ filteredCanti().length }}
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<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'"
|
||||
size="small"
|
||||
(click)="toggleComunitaFilter()"
|
||||
[color]="comunitaService.isFilterActive() ? 'secondary' : 'medium'"
|
||||
class="filter-chip"
|
||||
style="margin: 0;">
|
||||
<ion-icon slot="start" name="people-outline" style="font-size: 1.1rem; margin-right: 4px;"></ion-icon>
|
||||
{{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }}
|
||||
</ion-button>
|
||||
|
||||
<div
|
||||
*ngIf="comunitaService.comunitaCode()"
|
||||
(click)="editComunitaCode($event)"
|
||||
style="position: absolute; top: -6px; right: -6px; z-index: 99999; background: var(--ion-color-secondary); color: white; border-radius: 50%; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; border: 1.5px solid #1a1a1a; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"
|
||||
class="floating-edit-badge">
|
||||
<ion-icon name="create-outline" style="font-size: 0.85rem; pointer-events: none;"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-button
|
||||
*ngIf="settingsService.showEditor()"
|
||||
[fill]="showOnlyMine() ? 'solid' : 'outline'"
|
||||
@@ -183,10 +204,12 @@
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
class="transparent-list">
|
||||
|
||||
<!-- Premium Suggested Mass Card (Collapsible) -->
|
||||
<!-- Premium Suggested Mass Card (Collapsible with Swipe support) -->
|
||||
<div class="suggeriti-header-card glass ion-margin-bottom"
|
||||
*ngIf="showSuggeriti() && cantiLettureService.data()"
|
||||
(click)="isMassCardExpanded.set(!isMassCardExpanded())"
|
||||
(touchstart)="onMassCardTouchStart($event)"
|
||||
(touchend)="onMassCardTouchEnd($event)"
|
||||
[class.expanded]="isMassCardExpanded()">
|
||||
<div class="card-inner ion-padding">
|
||||
<div class="card-header-row">
|
||||
@@ -194,7 +217,17 @@
|
||||
<span class="card-label">SINTESI MESSA</span>
|
||||
<span class="card-date-compact" *ngIf="!isMassCardExpanded()">{{ getSelectedMassFormattedDate() }}</span>
|
||||
</div>
|
||||
<ion-icon [name]="isMassCardExpanded() ? 'chevron-up-outline' : 'chevron-down-outline'" class="expand-icon"></ion-icon>
|
||||
<div class="card-header-right" style="display: flex; align-items: center; gap: 8px;">
|
||||
<div class="mass-nav-buttons" style="display: flex; align-items: center; gap: 4px; z-index: 10;" (click)="$event.stopPropagation()">
|
||||
<ion-button fill="clear" size="small" color="secondary" (click)="navigateMassDate(-1)" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 28px;">
|
||||
<ion-icon slot="icon-only" name="chevron-back-outline" style="font-size: 1.1rem;"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" size="small" color="secondary" (click)="navigateMassDate(1)" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 28px;">
|
||||
<ion-icon slot="icon-only" name="chevron-forward-outline" style="font-size: 1.1rem;"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<ion-icon [name]="isMassCardExpanded() ? 'chevron-up-outline' : 'chevron-down-outline'" class="expand-icon" style="margin-left: 4px;"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="expandable-content" [class.show]="isMassCardExpanded()">
|
||||
@@ -234,7 +267,7 @@
|
||||
<div class="top-row">
|
||||
<ion-label class="info-section">
|
||||
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary)">
|
||||
{{ canto.titolo }}
|
||||
<span *ngIf="getCommunitySongNumber(canto)" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto) }}</span>{{ canto.titolo }}
|
||||
</h2>
|
||||
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||
<span>{{ canto.autore || 'Autore sconosciuto' }}</span>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
padding: 8px 4px 8px 0;
|
||||
|
||||
// Hide scrollbar but keep functionality
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
+229
-9
@@ -9,11 +9,12 @@ import { PlaylistService } from '../services/playlist.service';
|
||||
import { SettingsService } from '../services/settings.service';
|
||||
import { VERSION } from '../version';
|
||||
import { YoutubePlayerService } from '../services/youtube-player.service';
|
||||
import { ModalController, AlertController, ToastController } from '@ionic/angular';
|
||||
import { ModalController, AlertController, ToastController, LoadingController } from '@ionic/angular';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { MyCantiService } from '../services/my-canti.service';
|
||||
import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component';
|
||||
import { CantiLettureService } from '../services/canti-letture.service';
|
||||
import { ComunitaService } from '../services/comunita.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -38,6 +39,7 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
public fontSize = signal<number>(1.0);
|
||||
public youtubePlayerService = inject(YoutubePlayerService);
|
||||
public comunitaService = inject(ComunitaService);
|
||||
public isSeeking = false;
|
||||
|
||||
private readonly MIN_FONT = 0.6;
|
||||
@@ -59,6 +61,7 @@ export class HomePage implements OnDestroy {
|
||||
private modalCtrl = inject(ModalController);
|
||||
private alertCtrl = inject(AlertController);
|
||||
private toastCtrl = inject(ToastController);
|
||||
private loadingCtrl = inject(LoadingController);
|
||||
|
||||
private firstInteraction = true;
|
||||
|
||||
@@ -70,7 +73,14 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
private scrollEffect = effect(() => {
|
||||
const activeId = this.youtubePlayerService.currentCantoId();
|
||||
const list = this.filteredCanti();
|
||||
|
||||
if (activeId) {
|
||||
const index = list.findIndex(c => c.id === activeId);
|
||||
if (index !== -1 && index >= this.limit()) {
|
||||
this.limit.set(index + 15);
|
||||
}
|
||||
|
||||
// Delay slightly to ensure DOM is updated and classes are applied
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById('canto-' + activeId);
|
||||
@@ -79,7 +89,7 @@ export class HomePage implements OnDestroy {
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}, { allowSignalWrites: true });
|
||||
|
||||
private normalize(str: string | undefined): string {
|
||||
if (!str) return '';
|
||||
@@ -103,6 +113,25 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
||||
|
||||
// Filter by Community if code is set and filter is active
|
||||
const comunitaCode = this.comunitaService.comunitaCode();
|
||||
const comunitaIds = this.comunitaService.comunitaCantiIds();
|
||||
if (comunitaCode && this.comunitaService.isFilterActive()) {
|
||||
list = list.filter(c => {
|
||||
return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id);
|
||||
});
|
||||
|
||||
// Sort list by community song number (num_canto) ASC!
|
||||
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||
list.sort((a, b) => {
|
||||
const infoA = cantiInfo.find(x => x.id_canti === a.id_canti || x.id_canti === Number(a.id));
|
||||
const infoB = cantiInfo.find(x => x.id_canti === b.id_canti || x.id_canti === Number(b.id));
|
||||
const numA = infoA ? Number(infoA.num_canto) : 999999;
|
||||
const numB = infoB ? Number(infoB.num_canto) : 999999;
|
||||
return numA - numB;
|
||||
});
|
||||
}
|
||||
|
||||
if (onlyMine) {
|
||||
list = this.myCantiService.myCanti();
|
||||
}
|
||||
@@ -160,17 +189,21 @@ export class HomePage implements OnDestroy {
|
||||
// Converte "uno" in "1" per facilitare la ricerca vocale (es. "numero uno")
|
||||
const processedQuery = query.replace(/\buno\b/g, '1');
|
||||
|
||||
// Se la ricerca inizia con "numero" o "nr", filtra esattamente per id_canti
|
||||
// Se la ricerca inizia con "numero" o "nr", filtra esattamente per id_canti/comunita number
|
||||
const numberMatchPattern = processedQuery.match(/^(?:numero|nr\.?)\s*(\d+)$/);
|
||||
if (numberMatchPattern) {
|
||||
const targetId = numberMatchPattern[1];
|
||||
return list.filter(c => c.id_canti.toString() === targetId);
|
||||
return list.filter(c => {
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
|
||||
});
|
||||
}
|
||||
|
||||
return list.filter(c => {
|
||||
const titleMatch = this.normalize(c.titolo).includes(query);
|
||||
const authorMatch = this.normalize(c.autore).includes(query);
|
||||
const numberMatch = c.id_canti.toString().includes(query);
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query);
|
||||
|
||||
// Strip tags like {Chorus} and newlines from lyrics before searching
|
||||
const lyricsPlain = (c.testo || '')
|
||||
@@ -642,10 +675,6 @@ export class HomePage implements OnDestroy {
|
||||
setTimeout(() => {
|
||||
this.limit.update(l => l + 10);
|
||||
event.target.complete();
|
||||
|
||||
if (this.limit() >= this.filteredCanti().length) {
|
||||
event.target.disabled = true;
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -883,4 +912,195 @@ export class HomePage implements OnDestroy {
|
||||
this.limit.set(10);
|
||||
}
|
||||
|
||||
getCommunitySongNumber(canto: any): string | null {
|
||||
if (!this.comunitaService.comunitaCode() || !this.comunitaService.isFilterActive()) return null;
|
||||
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||
const info = cantiInfo.find(x => x.id_canti === canto.id_canti || x.id_canti === Number(canto.id));
|
||||
return info && info.num_canto ? info.num_canto.toString() : null;
|
||||
}
|
||||
|
||||
toggleComunitaFilter() {
|
||||
if (!this.comunitaService.comunitaCode()) {
|
||||
this.promptComunitaCode();
|
||||
} else {
|
||||
this.comunitaService.isFilterActive.update(v => !v);
|
||||
}
|
||||
}
|
||||
|
||||
editComunitaCode(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.promptComunitaCode();
|
||||
}
|
||||
|
||||
async promptComunitaCode() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Imposta Comunità',
|
||||
subHeader: 'Inserisci il codice parrocchiale/comunità per attivare il libretto dedicato:',
|
||||
cssClass: 'premium-alert',
|
||||
inputs: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'text',
|
||||
placeholder: 'Es: 123456',
|
||||
value: this.comunitaService.comunitaCode()
|
||||
}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
text: 'Annulla',
|
||||
role: 'cancel'
|
||||
},
|
||||
{
|
||||
text: 'Rimuovi',
|
||||
role: 'destructive',
|
||||
cssClass: 'alert-button-delete',
|
||||
handler: async () => {
|
||||
await this.comunitaService.setComunitaCode('');
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Comunità disattivata.',
|
||||
duration: 2000,
|
||||
color: 'secondary'
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Salva',
|
||||
handler: async (data) => {
|
||||
const trimmed = (data.code || '').trim();
|
||||
if (!trimmed) {
|
||||
await this.comunitaService.setComunitaCode('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading overlay with progress
|
||||
const loading = await this.loadingCtrl.create({
|
||||
message: 'Caricamento 0%',
|
||||
cssClass: 'premium-loading',
|
||||
spinner: 'crescent'
|
||||
});
|
||||
await loading.present();
|
||||
|
||||
// Subscribe to progress updates
|
||||
let progressInterval: any = null;
|
||||
progressInterval = setInterval(() => {
|
||||
const pct = this.comunitaService.loadingProgress();
|
||||
loading.message = `Caricamento ${pct}%`;
|
||||
if (pct >= 100) {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const success = await this.comunitaService.setComunitaCode(trimmed);
|
||||
clearInterval(progressInterval);
|
||||
await loading.dismiss();
|
||||
|
||||
if (success) {
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`,
|
||||
duration: 2000,
|
||||
color: 'success'
|
||||
});
|
||||
await toast.present();
|
||||
} else {
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Codice non trovato o errore di connessione.',
|
||||
duration: 2000,
|
||||
color: 'danger'
|
||||
});
|
||||
await toast.present();
|
||||
setTimeout(() => this.promptComunitaCode(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
private massCardStartX: number = 0;
|
||||
private massCardStartY: number = 0;
|
||||
|
||||
onMassCardTouchStart(event: TouchEvent) {
|
||||
if (event.touches.length === 1) {
|
||||
this.massCardStartX = event.touches[0].clientX;
|
||||
this.massCardStartY = event.touches[0].clientY;
|
||||
}
|
||||
}
|
||||
|
||||
onMassCardTouchEnd(event: TouchEvent) {
|
||||
if (event.changedTouches.length === 1) {
|
||||
const endX = event.changedTouches[0].clientX;
|
||||
const endY = event.changedTouches[0].clientY;
|
||||
const diffX = endX - this.massCardStartX;
|
||||
const diffY = endY - this.massCardStartY;
|
||||
|
||||
// Ensure it is a horizontal swipe (diffX is much larger than diffY)
|
||||
if (Math.abs(diffX) > 60 && Math.abs(diffY) < 40) {
|
||||
// Prevent triggering click expansion
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (diffX > 0) {
|
||||
// Swiped right -> go to PREVIOUS date
|
||||
this.navigateMassDate(-1);
|
||||
} else {
|
||||
// Swiped left -> go to NEXT date
|
||||
this.navigateMassDate(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async navigateMassDate(direction: number) {
|
||||
const list = this.cantiLettureService.availableMasses();
|
||||
if (list.length === 0) return;
|
||||
|
||||
const currentDate = this.cantiLettureService.selectedMassDate();
|
||||
if (!currentDate) {
|
||||
this.cantiLettureService.selectedMassDate.set(list[0].date);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = list.findIndex(m => m.date === currentDate);
|
||||
if (currentIndex === -1) {
|
||||
this.cantiLettureService.selectedMassDate.set(list[0].date);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = currentIndex + direction;
|
||||
if (nextIndex >= 0 && nextIndex < list.length) {
|
||||
// Set the next/prev date
|
||||
this.cantiLettureService.selectedMassDate.set(list[nextIndex].date);
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: `Messa: ${list[nextIndex].day} ${this.formatCompactDate(list[nextIndex].date)}`,
|
||||
duration: 1500,
|
||||
color: 'secondary',
|
||||
position: 'bottom'
|
||||
});
|
||||
await toast.present();
|
||||
} else {
|
||||
// "quando le messe sono finite rimani sull'ultima valorizzata, non dare errore"
|
||||
const targetIndex = direction > 0 ? list.length - 1 : 0;
|
||||
this.cantiLettureService.selectedMassDate.set(list[targetIndex].date);
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: direction > 0 ? 'Nessun\'altra messa futura disponibile.' : 'Nessun\'altra messa passata disponibile.',
|
||||
duration: 1500,
|
||||
color: 'medium',
|
||||
position: 'bottom'
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
}
|
||||
|
||||
private formatCompactDate(dateStr: string): string {
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}`;
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
||||
</div>
|
||||
<ion-button fill="clear" (click)="toggleChords()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
|
||||
<ion-icon slot="icon-only"
|
||||
[name]="showChords() ? 'musical-notes-outline' : 'text-outline'"
|
||||
[color]="showChords() ? 'secondary' : 'medium'"
|
||||
style="font-size: 1.3rem;">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title class="outfit-font wrapped-title">
|
||||
<div class="title-main" [style.fontSize.rem]="fontSize() * 1.1">
|
||||
<span class="canto-number" *ngIf="canto()?.id_canti">{{ canto()?.id_canti }}</span>
|
||||
<span class="canto-number" *ngIf="canto()?.id_canti" [style.color]="getCommunitySongNumber(canto()) !== null ? 'var(--ion-color-secondary)' : ''">
|
||||
{{ getCommunitySongNumber(canto()) || canto()?.id_canti }}
|
||||
</span>
|
||||
<span class="title-text">{{ canto()?.titolo || 'Player' }}</span>
|
||||
</div>
|
||||
</ion-title>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { ConnectivityService } from '../../services/connectivity.service';
|
||||
import { PlaylistService } from '../../services/playlist.service';
|
||||
import { YoutubePlayerService } from '../../services/youtube-player.service';
|
||||
import { MyCantiService } from '../../services/my-canti.service';
|
||||
import { ComunitaService } from '../../services/comunita.service';
|
||||
import { StatsService } from '../../services/stats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-player',
|
||||
@@ -103,11 +105,15 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
public settingsService = inject(SettingsService);
|
||||
public connectivityService = inject(ConnectivityService);
|
||||
public playlistService = inject(PlaylistService);
|
||||
public comunitaService = inject(ComunitaService);
|
||||
private myCantiService = inject(MyCantiService);
|
||||
private statsService = inject(StatsService);
|
||||
private gestureCtrl = inject(GestureController);
|
||||
private el = inject(ElementRef);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
private songStartTime: number = 0;
|
||||
|
||||
constructor() {
|
||||
// Sync mode from global settings
|
||||
effect(() => {
|
||||
@@ -195,9 +201,24 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (found) {
|
||||
this.logPreviousSongTime();
|
||||
this.canto.set(found);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -243,11 +264,21 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
@@ -485,7 +516,25 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getCommunitySongNumber(canto: any): string | null {
|
||||
if (!canto) return null;
|
||||
if (!this.comunitaService.comunitaCode() || !this.comunitaService.isFilterActive()) return null;
|
||||
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||
const info = cantiInfo.find(x => x.id_canti === canto.id_canti || x.id_canti === Number(canto.id));
|
||||
return info && info.num_canto ? info.num_canto.toString() : null;
|
||||
}
|
||||
|
||||
private logPreviousSongTime() {
|
||||
const c = this.canto();
|
||||
if (c && this.songStartTime > 0) {
|
||||
const timeSpent = (Date.now() - this.songStartTime) / 1000;
|
||||
this.statsService.logSongView(c.id_canti, timeSpent);
|
||||
this.songStartTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.logPreviousSongTime();
|
||||
this.audioEngine.stopListening();
|
||||
this.channel.close();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
</div>
|
||||
<div class="song-info">
|
||||
<span class="canto-number">{{ song.id_canti }}</span>
|
||||
<span class="song-title">{{ song.titolo }}</span>
|
||||
<span class="song-title">
|
||||
<span *ngIf="getCommunitySongNumber(song)" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 6px;">{{ getCommunitySongNumber(song) }}</span>{{ song.titolo }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="example-custom-placeholder" *cdkDragPlaceholder></div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { PlaylistService } from '../../services/playlist.service';
|
||||
import { CantiService } from '../../services/canti.service';
|
||||
import { ComunitaService } from '../../services/comunita.service';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { AlertController, ToastController, ModalController } from '@ionic/angular';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -14,6 +15,7 @@ import { Router } from '@angular/router';
|
||||
export class PlaylistPage {
|
||||
public playlistService = inject(PlaylistService);
|
||||
public cantiService = inject(CantiService);
|
||||
public comunitaService = inject(ComunitaService);
|
||||
private alertCtrl = inject(AlertController);
|
||||
private toastCtrl = inject(ToastController);
|
||||
private router = inject(Router);
|
||||
@@ -177,4 +179,11 @@ export class PlaylistPage {
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
getCommunitySongNumber(song: any): string | null {
|
||||
if (!this.comunitaService.comunitaCode() || !this.comunitaService.isFilterActive()) return null;
|
||||
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||
const info = cantiInfo.find(x => x.id_canti === song.id_canti || x.id_canti === Number(song.id));
|
||||
return info && info.num_canto ? info.num_canto.toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,37 +61,31 @@
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- Messa di Riferimento Selection -->
|
||||
<!-- Comunità -->
|
||||
<div class="settings-group glass ion-margin-bottom">
|
||||
<div class="group-header ion-padding-start ion-padding-top">
|
||||
<h2 class="outfit-font settings-group-title">
|
||||
Messa di Riferimento (Suggeriti)
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="select-item-container ion-padding-horizontal ion-padding-bottom">
|
||||
<div class="select-label-row">
|
||||
<ion-icon name="calendar-outline" color="secondary" class="select-icon"></ion-icon>
|
||||
<div class="select-text-group">
|
||||
<h2 class="settings-item-title outfit-font">Seleziona Messa</h2>
|
||||
<p class="settings-item-subtitle outfit-font">Scegli la messa per i canti consigliati</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="select-box-wrapper">
|
||||
<ion-select [value]="cantiLettureService.selectedMassDate()"
|
||||
(ionChange)="onMassChange($event)"
|
||||
interface="action-sheet"
|
||||
class="custom-select-block"
|
||||
placeholder="Seleziona una messa...">
|
||||
<ion-select-option *ngFor="let mass of cantiLettureService.availableMasses()" [value]="mass.date">
|
||||
{{ mass.formattedLabel }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</div>
|
||||
</div>
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
<ion-icon name="people-outline" slot="start" color="secondary"></ion-icon>
|
||||
<ion-label class="outfit-font">
|
||||
<h2 class="settings-item-title">Comunità</h2>
|
||||
<p class="settings-item-subtitle">Mostra il filtro Comunità nella home</p>
|
||||
</ion-label>
|
||||
<ion-toggle slot="end" [checked]="settingsService.comunitaEnabled()" (ionChange)="settingsService.toggleComunitaEnabled()" color="secondary"></ion-toggle>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- Invio Dati Statistici -->
|
||||
<div class="settings-group glass ion-margin-bottom">
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
<ion-icon name="analytics-outline" slot="start" color="secondary"></ion-icon>
|
||||
<ion-label class="outfit-font">
|
||||
<h2 class="settings-item-title">Invio dati statistici</h2>
|
||||
<p class="settings-item-subtitle">Invia statistiche di utilizzo e tonalità</p>
|
||||
</ion-label>
|
||||
<ion-toggle slot="end" [checked]="settingsService.invioDatiStatistici()" (ionChange)="settingsService.toggleInvioDatiStatistici()" color="secondary"></ion-toggle>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Display Mode Section -->
|
||||
<div class="settings-group glass ion-margin-bottom">
|
||||
<div class="group-header ion-padding-start ion-padding-top">
|
||||
@@ -117,6 +111,8 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="settings-group glass ion-margin-top" *ngIf="settingsService.showEditor()">
|
||||
<ion-item class="transparent-item" lines="none" (click)="myCantiService.sendAllMyCanti()" detail="true" button *ngIf="myCantiService.myCanti().length > 0">
|
||||
<ion-icon name="send-outline" slot="start" color="secondary"></ion-icon>
|
||||
@@ -185,6 +181,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend-item">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center;">
|
||||
<div class="legend-icon-wrapper">
|
||||
<ion-icon name="text-outline" color="medium"></ion-icon>
|
||||
</div>
|
||||
<div class="legend-icon-wrapper">
|
||||
<ion-icon name="musical-notes-outline" color="secondary"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-text" style="padding-top: 6px;">
|
||||
<h4 class="outfit-font">Testo / Accordi (icona in alto a destra)</h4>
|
||||
<p class="outfit-font">
|
||||
Alterna la visualizzazione tra solo testo e testo con accordi. L'icona con la <strong>T</strong> (grigia) indica la modalità testo; l'icona con le note musicali (arancione) indica la modalità accordi. Un tocco cambia modalità all'istante.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon-wrapper">
|
||||
<ion-icon name="arrow-up-circle" color="secondary"></ion-icon>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Router } from '@angular/router';
|
||||
|
||||
import { MyCantiService } from '../../services/my-canti.service';
|
||||
import { CantiLettureService } from '../../services/canti-letture.service';
|
||||
import { ComunitaService } from '../../services/comunita.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@@ -26,6 +27,7 @@ export class SettingsPage {
|
||||
public connectivityService = inject(ConnectivityService);
|
||||
public playlistService = inject(PlaylistService);
|
||||
public cantiLettureService = inject(CantiLettureService);
|
||||
public comunitaService = inject(ComunitaService);
|
||||
private swUpdate = inject(SwUpdate);
|
||||
private toastCtrl = inject(ToastController);
|
||||
private modalCtrl = inject(ModalController);
|
||||
@@ -47,6 +49,13 @@ export class SettingsPage {
|
||||
async fullRefresh() {
|
||||
// 1. Refresh JSON data
|
||||
this.cantiService.refresh();
|
||||
|
||||
// 2. Refresh liturgical readings JSON
|
||||
try {
|
||||
await this.cantiLettureService.fetchData();
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh liturgical readings:', err);
|
||||
}
|
||||
|
||||
// 2. Check for Service Worker updates
|
||||
if (this.swUpdate.isEnabled) {
|
||||
@@ -77,5 +86,4 @@ export class SettingsPage {
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -180,20 +180,20 @@ export class CantiLettureService {
|
||||
|
||||
this.availableMasses.set(massesList);
|
||||
|
||||
// If no mass is currently selected, select the first one (or today's mass if exists)
|
||||
if (!this.selectedMassDate() && massesList.length > 0) {
|
||||
// Try to find today's date
|
||||
// Always select today's mass if available on load, else find closest future date
|
||||
if (massesList.length > 0) {
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
const match = massesList.find(m => m.date === todayStr);
|
||||
if (match) {
|
||||
this.selectedMassDate.set(match.date);
|
||||
} else {
|
||||
// Find the first date that is today or in the future
|
||||
// If today is not present, find the first date that is in the future
|
||||
const futureMatch = massesList.find(m => m.date >= todayStr);
|
||||
if (futureMatch) {
|
||||
this.selectedMassDate.set(futureMatch.date);
|
||||
} else {
|
||||
this.selectedMassDate.set(massesList[0].date);
|
||||
// Otherwise fall back to the last available mass date (closest to today)
|
||||
this.selectedMassDate.set(massesList[massesList.length - 1].date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Injectable, signal, inject, effect } from '@angular/core';
|
||||
import { HttpClient, HttpRequest, HttpEventType, HttpResponse } from '@angular/common/http';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
export interface ParrocchiaItem {
|
||||
id_parrocchia: number;
|
||||
nome: string;
|
||||
codice: string;
|
||||
mail: string;
|
||||
guid_parrocchia: string;
|
||||
}
|
||||
|
||||
export interface ParrocchiaCantiItem {
|
||||
id_parrocchia: number;
|
||||
id_canti: number;
|
||||
num_canto: number;
|
||||
}
|
||||
|
||||
export interface CantiSettingsItem {
|
||||
id_canti: number;
|
||||
speed: number;
|
||||
tonalita: number;
|
||||
}
|
||||
|
||||
export interface GetAllAppTablesResponse {
|
||||
parrocchia?: { data: ParrocchiaItem[] };
|
||||
parrocchia_canti?: { data: ParrocchiaCantiItem[] };
|
||||
canti_settings?: { data: CantiSettingsItem[] };
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ComunitaService {
|
||||
private http = inject(HttpClient);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
public comunitaCode = signal<string>('');
|
||||
public comunitaNome = signal<string>('');
|
||||
public comunitaMail = signal<string>('');
|
||||
public comunitaCantiIds = signal<(number | string)[]>([]);
|
||||
public comunitaCantiInfo = signal<ParrocchiaCantiItem[]>([]);
|
||||
public comunitaCantiSettings = signal<CantiSettingsItem[]>([]);
|
||||
public isFilterActive = signal<boolean>(false);
|
||||
public loading = signal<boolean>(false);
|
||||
public loadingProgress = signal<number>(0);
|
||||
|
||||
constructor() {
|
||||
const savedCode = localStorage.getItem('comunita-code');
|
||||
const savedNome = localStorage.getItem('comunita-nome');
|
||||
const savedMail = localStorage.getItem('comunita-mail');
|
||||
const savedCanti = localStorage.getItem('comunita-canti-ids');
|
||||
const savedInfo = localStorage.getItem('comunita-canti-info');
|
||||
const savedSettings = localStorage.getItem('comunita-canti-settings');
|
||||
const savedFilterActive = localStorage.getItem('comunita-filter-active') === 'true';
|
||||
|
||||
if (savedCode) this.comunitaCode.set(savedCode);
|
||||
if (savedNome) this.comunitaNome.set(savedNome);
|
||||
if (savedMail) this.comunitaMail.set(savedMail);
|
||||
if (savedCanti) {
|
||||
try { this.comunitaCantiIds.set(JSON.parse(savedCanti)); } catch (e) {}
|
||||
}
|
||||
if (savedInfo) {
|
||||
try { this.comunitaCantiInfo.set(JSON.parse(savedInfo)); } catch (e) {}
|
||||
}
|
||||
if (savedSettings) {
|
||||
try { this.comunitaCantiSettings.set(JSON.parse(savedSettings)); } catch (e) {}
|
||||
}
|
||||
this.isFilterActive.set(savedFilterActive);
|
||||
|
||||
effect(() => {
|
||||
localStorage.setItem('comunita-code', this.comunitaCode());
|
||||
localStorage.setItem('comunita-nome', this.comunitaNome());
|
||||
localStorage.setItem('comunita-mail', this.comunitaMail());
|
||||
localStorage.setItem('comunita-canti-ids', JSON.stringify(this.comunitaCantiIds()));
|
||||
localStorage.setItem('comunita-canti-info', JSON.stringify(this.comunitaCantiInfo()));
|
||||
localStorage.setItem('comunita-canti-settings', JSON.stringify(this.comunitaCantiSettings()));
|
||||
localStorage.setItem('comunita-filter-active', this.isFilterActive().toString());
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!this.settingsService.comunitaEnabled()) {
|
||||
this.isFilterActive.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fetchWithProgress<T>(url: string): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const req = new HttpRequest('GET', url, {
|
||||
reportProgress: true,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
this.http.request<T>(req).subscribe({
|
||||
next: (event) => {
|
||||
if (event.type === HttpEventType.DownloadProgress) {
|
||||
if (event.total && event.total > 0) {
|
||||
const pct = Math.round((event.loaded / event.total) * 100);
|
||||
this.loadingProgress.set(pct);
|
||||
} else {
|
||||
// No Content-Length header — simulate gradual progress up to 85%
|
||||
const simulated = Math.min(85, this.loadingProgress() + 12);
|
||||
this.loadingProgress.set(simulated);
|
||||
}
|
||||
} else if (event instanceof HttpResponse) {
|
||||
this.loadingProgress.set(100);
|
||||
resolve(event.body as T);
|
||||
}
|
||||
},
|
||||
error: (err) => reject(err)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async setComunitaCode(code: string): Promise<boolean> {
|
||||
const trimmedCode = code.trim();
|
||||
if (!trimmedCode) {
|
||||
this.comunitaCode.set('');
|
||||
this.comunitaNome.set('');
|
||||
this.comunitaMail.set('');
|
||||
this.comunitaCantiIds.set([]);
|
||||
this.comunitaCantiInfo.set([]);
|
||||
this.comunitaCantiSettings.set([]);
|
||||
this.isFilterActive.set(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
this.loadingProgress.set(0);
|
||||
|
||||
// 1. Try to fetch from the actual production API
|
||||
try {
|
||||
const url = `https://libretto.mmcinet.eu/canti/api/v3/get_all_app_tables?uuid=pwa-cc-uuid&email=&platform=browser&version=1.0&gruppo=${trimmedCode}&all_song=false&t=${Date.now()}`;
|
||||
const data = await this.fetchWithProgress<GetAllAppTablesResponse>(url);
|
||||
|
||||
if (data && data.parrocchia?.data && data.parrocchia.data.length > 0) {
|
||||
const parrocchia = data.parrocchia.data[0];
|
||||
const cantiList = data.parrocchia_canti?.data || [];
|
||||
const settingsList = data.canti_settings?.data || [];
|
||||
|
||||
this.comunitaCode.set(trimmedCode);
|
||||
this.comunitaNome.set(parrocchia.nome || `Comunità ${trimmedCode}`);
|
||||
this.comunitaMail.set(parrocchia.mail || '');
|
||||
this.comunitaCantiInfo.set(cantiList);
|
||||
this.comunitaCantiIds.set(cantiList.map(c => c.id_canti));
|
||||
this.comunitaCantiSettings.set(settingsList);
|
||||
this.isFilterActive.set(true);
|
||||
this.loading.set(false);
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch from production API, trying fallback...', err);
|
||||
this.loadingProgress.set(0);
|
||||
}
|
||||
|
||||
// 2. Fallback to static community JSON
|
||||
try {
|
||||
const isProduction = window.location.hostname.includes('canticristiani.it');
|
||||
const origin = isProduction ? window.location.origin : 'https://www.canticristiani.it';
|
||||
const fallbackUrl = `${origin}/api/comunita_${trimmedCode}.json?t=${Date.now()}`;
|
||||
|
||||
interface StaticComunitaData {
|
||||
id_comunita: string;
|
||||
nome_comunita: string;
|
||||
canti: (number | string)[];
|
||||
}
|
||||
|
||||
const staticData = await this.fetchWithProgress<StaticComunitaData>(fallbackUrl);
|
||||
if (staticData && staticData.canti) {
|
||||
this.comunitaCode.set(trimmedCode);
|
||||
this.comunitaNome.set(staticData.nome_comunita || `Comunità ${trimmedCode}`);
|
||||
this.comunitaCantiIds.set(staticData.canti);
|
||||
|
||||
const mockInfo = staticData.canti.map((id, index) => ({
|
||||
id_parrocchia: 1,
|
||||
id_canti: Number(id),
|
||||
num_canto: index + 1
|
||||
}));
|
||||
|
||||
this.comunitaCantiInfo.set(mockInfo);
|
||||
this.comunitaCantiSettings.set([]);
|
||||
this.isFilterActive.set(true);
|
||||
this.loading.set(false);
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('All community fetch strategies failed:', err);
|
||||
}
|
||||
|
||||
this.loading.set(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,14 @@ export class SettingsService {
|
||||
public fullscreenMode = signal<boolean>(false);
|
||||
|
||||
/** Browser Fullscreen state: true = fullscreen active */
|
||||
public browserFullscreen = signal<boolean>(!!document.fullscreenElement);
|
||||
public browserFullscreen = signal<boolean>(
|
||||
!!(
|
||||
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<boolean>(true);
|
||||
@@ -22,6 +29,12 @@ export class SettingsService {
|
||||
/** Schermo sempre acceso: true = attiva Screen Wake Lock */
|
||||
public keepScreenOn = signal<boolean>(false);
|
||||
|
||||
/** Funzionalità Comunità: true = il chip comunità è visibile nella home */
|
||||
public comunitaEnabled = signal<boolean>(true);
|
||||
|
||||
/** Invio dati statistici: true = invia pacchetto dati statistici */
|
||||
public invioDatiStatistici = signal<boolean>(false);
|
||||
|
||||
private wakeLock: any = null;
|
||||
|
||||
constructor() {
|
||||
@@ -53,10 +66,31 @@ export class SettingsService {
|
||||
this.keepScreenOn.set(savedKeepScreenOn === 'true');
|
||||
}
|
||||
|
||||
// Sync browser fullscreen state with listeners
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.browserFullscreen.set(!!document.fullscreenElement);
|
||||
});
|
||||
const savedComunitaEnabled = localStorage.getItem('comunita-enabled');
|
||||
if (savedComunitaEnabled !== null) {
|
||||
this.comunitaEnabled.set(savedComunitaEnabled === 'true');
|
||||
}
|
||||
|
||||
const savedStats = localStorage.getItem('invio-dati-statistici');
|
||||
if (savedStats !== null) {
|
||||
this.invioDatiStatistici.set(savedStats === 'true');
|
||||
}
|
||||
|
||||
// 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());
|
||||
@@ -110,6 +144,12 @@ export class SettingsService {
|
||||
this.keepScreenOn.update(v => !v);
|
||||
}
|
||||
|
||||
toggleComunitaEnabled() {
|
||||
const newValue = !this.comunitaEnabled();
|
||||
this.comunitaEnabled.set(newValue);
|
||||
localStorage.setItem('comunita-enabled', newValue.toString());
|
||||
}
|
||||
|
||||
private async requestWakeLock() {
|
||||
if ('wakeLock' in navigator) {
|
||||
try {
|
||||
@@ -128,14 +168,41 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
toggleBrowserFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(err => {
|
||||
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||||
});
|
||||
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 {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Injectable, inject, effect } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { ConnectivityService } from './connectivity.service';
|
||||
import { ComunitaService } from './comunita.service';
|
||||
import { VERSION } from '../version';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StatsService {
|
||||
private http = inject(HttpClient);
|
||||
private settingsService = inject(SettingsService);
|
||||
private connectivityService = inject(ConnectivityService);
|
||||
private comunitaService = inject(ComunitaService);
|
||||
|
||||
constructor() {
|
||||
// Automatically trigger sync when the device becomes online
|
||||
effect(() => {
|
||||
if (this.connectivityService.isOnline()) {
|
||||
this.syncStats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getOrCreateUUID(): string {
|
||||
let uuid = localStorage.getItem('stats-uuid');
|
||||
if (!uuid) {
|
||||
uuid = '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('stats-uuid', uuid);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
private formatDateTime(date: Date): string {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
const y = date.getFullYear();
|
||||
const m = pad(date.getMonth() + 1);
|
||||
const d = pad(date.getDate());
|
||||
const h = date.getHours();
|
||||
const min = pad(date.getMinutes());
|
||||
const s = pad(date.getSeconds());
|
||||
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
||||
}
|
||||
|
||||
private getBrowserModel(): string {
|
||||
const ua = navigator.userAgent;
|
||||
if (ua.includes('Chrome')) return 'Chrome';
|
||||
if (ua.includes('Safari') && !ua.includes('Chrome')) return 'Safari';
|
||||
if (ua.includes('Firefox')) return 'Firefox';
|
||||
return 'Browser';
|
||||
}
|
||||
|
||||
private getLocalLogs(): any[] {
|
||||
const data = localStorage.getItem('stats-logs-queue');
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
private saveLocalLogs(logs: any[]) {
|
||||
localStorage.setItem('stats-logs-queue', JSON.stringify(logs));
|
||||
}
|
||||
|
||||
private clearLocalLogs() {
|
||||
localStorage.removeItem('stats-logs-queue');
|
||||
}
|
||||
|
||||
private getLocalSongSettings(): any[] {
|
||||
const data = localStorage.getItem('stats-settings-queue');
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
private saveLocalSongSettings(settings: any[]) {
|
||||
localStorage.setItem('stats-settings-queue', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
logSongView(idCanti: number, timeSpentSeconds: number) {
|
||||
if (!this.settingsService.invioDatiStatistici()) return;
|
||||
if (timeSpentSeconds < 1) return; // Ignore very short views
|
||||
|
||||
console.log('📊 StatsService: logSongView called for song', idCanti, 'timespent:', timeSpentSeconds);
|
||||
|
||||
const logEntry = {
|
||||
uuid: this.getOrCreateUUID(),
|
||||
id_canti: idCanti.toString(),
|
||||
data_log: this.formatDateTime(new Date()),
|
||||
version: VERSION,
|
||||
tempo: Math.round(timeSpentSeconds),
|
||||
lat: null,
|
||||
lon: null,
|
||||
json: JSON.stringify({
|
||||
model: this.getBrowserModel(),
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth
|
||||
}),
|
||||
stato: 0,
|
||||
id: Math.floor(Math.random() * 100000)
|
||||
};
|
||||
|
||||
const logs = this.getLocalLogs();
|
||||
logs.push(logEntry);
|
||||
this.saveLocalLogs(logs);
|
||||
|
||||
this.syncStats();
|
||||
}
|
||||
|
||||
updateSongSettings(idCanti: number, tonalita: number, speed: number = 0) {
|
||||
if (!this.settingsService.invioDatiStatistici()) return;
|
||||
|
||||
console.log('📊 StatsService: updateSongSettings called for song', idCanti, 'tonalita:', tonalita);
|
||||
|
||||
const settingsList = this.getLocalSongSettings();
|
||||
const idx = settingsList.findIndex(s => s.id_canti === idCanti);
|
||||
|
||||
if (idx !== -1) {
|
||||
settingsList[idx].tonalita = tonalita;
|
||||
settingsList[idx].speed = speed;
|
||||
} else {
|
||||
settingsList.push({
|
||||
id_canti: idCanti,
|
||||
speed: speed,
|
||||
tonalita: tonalita,
|
||||
id: Math.floor(Math.random() * 100000)
|
||||
});
|
||||
}
|
||||
|
||||
this.saveLocalSongSettings(settingsList);
|
||||
this.syncStats();
|
||||
}
|
||||
|
||||
private clearLocalStats() {
|
||||
localStorage.removeItem('stats-logs-queue');
|
||||
localStorage.removeItem('stats-settings-queue');
|
||||
}
|
||||
|
||||
syncStats() {
|
||||
if (!this.settingsService.invioDatiStatistici()) {
|
||||
console.log('📊 StatsService: Sync ignored (settings disabled)');
|
||||
return;
|
||||
}
|
||||
if (!this.connectivityService.isOnline()) {
|
||||
console.log('📊 StatsService: Sync postponed (offline)');
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = this.getLocalLogs();
|
||||
const settings = this.getLocalSongSettings();
|
||||
|
||||
console.log('📊 StatsService: syncStats checking queues. Logs:', logs.length, 'Settings:', settings.length);
|
||||
|
||||
if (logs.length === 0 && settings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uuid = this.getOrCreateUUID();
|
||||
const email = this.comunitaService.comunitaMail() || `pwa-${uuid.substring(0,8)}@librettocanti.it`;
|
||||
|
||||
const payload = {
|
||||
uuid: uuid,
|
||||
log: logs,
|
||||
pref: [],
|
||||
canti_settings: settings,
|
||||
user_devices: {
|
||||
uuid: uuid,
|
||||
email: email,
|
||||
platform: 'browser',
|
||||
version: VERSION,
|
||||
gruppo: (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) ? this.comunitaService.comunitaCode() : '',
|
||||
all_song: false,
|
||||
deleteList: false,
|
||||
deletePref: false,
|
||||
data_app_sync: this.formatDateTime(new Date())
|
||||
}
|
||||
};
|
||||
|
||||
const url = `https://api.librettocanti.it/canti/api/v1/updateOnlineDB?uuid=${uuid}&email=${email}`;
|
||||
console.log('📊 StatsService: Sending POST request to:', url, 'with payload:', payload);
|
||||
|
||||
this.http.post(url, payload).subscribe({
|
||||
next: (res) => {
|
||||
this.clearLocalStats();
|
||||
console.log('📊 StatsService: Sync successful!', res);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('📊 StatsService: Sync failed, keeping in local buffer...', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const VERSION = '2026.05.18.0125';
|
||||
export const VERSION = '2026.05.18.1550';
|
||||
|
||||
Reference in New Issue
Block a user