modificato deploy verso contabo

This commit is contained in:
David Frassi
2026-06-17 01:04:27 +02:00
parent 2d23dcae83
commit 97a11d5074
24 changed files with 1063 additions and 64 deletions
+13
View File
@@ -309,6 +309,9 @@ export class AppComponent implements OnInit {
if (!skipRedirect) {
this.showRedirectOverlay.set(true);
this.redirectFailed.set(false);
if ((window as any).PwaLoader) {
(window as any).PwaLoader.update({ isRedirect: true });
}
// Tentiamo il reindirizzamento automatico
setTimeout(() => {
window.location.href = this.protocolLink;
@@ -318,6 +321,9 @@ export class AppComponent implements OnInit {
if (this.showRedirectOverlay()) {
this.redirectFailed.set(true);
localStorage.setItem('pwa-installed', 'false');
if ((window as any).PwaLoader) {
(window as any).PwaLoader.hide();
}
}
}, 2000);
}, 800);
@@ -335,11 +341,18 @@ export class AppComponent implements OnInit {
openPwaManual() {
window.location.href = this.protocolLink;
if ((window as any).PwaLoader) {
(window as any).PwaLoader.show();
(window as any).PwaLoader.update({ isRedirect: true });
}
// Se dopo 2 secondi l'utente è ancora qui, probabilmente l'app non è installata
setTimeout(() => {
if (this.showRedirectOverlay()) {
this.redirectFailed.set(true);
localStorage.setItem('pwa-installed', 'false');
if ((window as any).PwaLoader) {
(window as any).PwaLoader.hide();
}
}
}, 2000);
}
+170 -12
View File
@@ -278,11 +278,12 @@ export class HomePage implements OnDestroy {
const comunitaIds = this.comunitaService.comunitaCantiIds();
if (comunitaCode && this.comunitaService.isFilterActive()) {
// Base is standard canti + my canti + community custom canti + remote custom canti
// Base is standard canti + my canti + community custom canti + remote custom canti + remote share canti
list = [
...this.cantiService.canti(),
...this.myCantiService.myCanti(),
...this.playlistService.remoteCustomSongs(),
...this.playlistService.remoteShareCanti(),
...this.comunitaService.comunitaCantiPersonali()
];
@@ -316,11 +317,12 @@ export class HomePage implements OnDestroy {
return numA - numB;
});
} else {
// General context: only standard canti + my canti + remote custom canti
// General context: only standard canti + my canti + remote custom canti + remote share canti
list = [
...this.cantiService.canti(),
...this.myCantiService.myCanti(),
...this.playlistService.remoteCustomSongs()
...this.playlistService.remoteCustomSongs(),
...this.playlistService.remoteShareCanti()
];
}
@@ -488,9 +490,21 @@ export class HomePage implements OnDestroy {
if (this.settingsService.isVersionCheckComplete() && !queryParamsSubscribed) {
queryParamsSubscribed = true;
this.queryParamsSubscription = this.route.queryParams.subscribe(params => {
if (params['id']) {
const idVal = params['id'];
this.router.navigate([], { queryParams: { id: null, t: null }, queryParamsHandling: 'merge', replaceUrl: true }).then(() => {
this.goToCanto(idVal);
});
}
if (params['import']) {
this.handleImport(params['import']);
}
if (params['import-canto']) {
this.handleImportCanto(params['import-canto']);
}
if (params['song-uid'] && params['song-id']) {
this.handleRemoteSongImport(params['song-uid'], params['song-id']);
}
if (params['playlist-uid']) {
this.handleRemotePlaylistImport(params['playlist-uid'], params['playlist-id']);
}
@@ -508,7 +522,7 @@ export class HomePage implements OnDestroy {
if (versionReady && lastPl && !hasAutoActivatedPlaylist) {
const params = this.route.snapshot.queryParams;
if (!params['id'] && !params['import'] && !params['playlist-uid'] && !params['restore-uid']) {
if (!params['id'] && !params['import'] && !params['playlist-uid'] && !params['restore-uid'] && !params['song-uid'] && !params['import-canto']) {
hasAutoActivatedPlaylist = true;
this.selectPlaylist(lastPl);
this.activeFilterType.set('playlist');
@@ -581,7 +595,8 @@ export class HomePage implements OnDestroy {
async handleImport(base64: string) {
try {
const decoded = decodeURIComponent(escape(atob(base64)));
const base64Safe = base64.replace(/ /g, '+');
const decoded = decodeURIComponent(escape(atob(base64Safe)));
const json = JSON.parse(decoded);
if (json && json.comunitaCode) {
@@ -630,11 +645,12 @@ export class HomePage implements OnDestroy {
}
}
const openFirst = this.route.snapshot.queryParams['openFirst'] === '1';
if (this.playlistService.processImportJson(json)) {
this.limit.set(50); // Mostra più canti inizialmente per le playlist speciali
this.activeFilterType.set('playlist');
await this.router.navigate([], { queryParams: { import: null }, queryParamsHandling: 'merge', replaceUrl: true });
if (json.ids && json.ids.length > 0) {
await this.router.navigate([], { queryParams: { import: null, openFirst: null }, queryParamsHandling: 'merge', replaceUrl: true });
if (openFirst && json.ids && json.ids.length > 0) {
this.goToCanto(json.ids[0]);
}
}
@@ -650,6 +666,117 @@ export class HomePage implements OnDestroy {
}
}
async handleImportCanto(base64: string) {
try {
const base64Safe = base64.replace(/ /g, '+');
const decoded = decodeURIComponent(escape(atob(base64Safe)));
const json = JSON.parse(decoded);
if (json && json.cantoShare && json.titolo) {
const id_canti = Date.now();
const id = `remote_share_${id_canti}`;
const canto: any = {
id: id,
id_canti: id_canti,
titolo: json.titolo,
autore: json.autore || '',
testo: json.testo || '',
accordi: json.accordi || '',
link_youtube: json.link_youtube || '',
id_momenti: json.id_momenti || [],
nonValidato: true
};
await this.playlistService.saveRemoteShareCanto(canto);
const toast = await this.toastCtrl.create({
message: `Canto "${canto.titolo}" importato nei brani remoti non validati!`,
duration: 3000,
color: 'success',
position: 'bottom'
});
await toast.present();
await this.router.navigate([], { queryParams: { 'import-canto': null }, queryParamsHandling: 'merge', replaceUrl: true });
this.goToCanto(canto.id);
}
} catch (e) {
console.error('Failed to import canto', e);
const toast = await this.toastCtrl.create({
message: 'Errore durante l\'importazione del canto.',
duration: 3000,
color: 'danger',
position: 'bottom'
});
await toast.present();
}
}
async handleRemoteSongImport(uid: string, songId: string) {
const loading = await this.loadingCtrl.create({
message: 'Scaricamento canto da remoto...'
});
await loading.present();
try {
const response = await fetch(`https://api.canticristiani.it/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Risposta del server non valida: ${response.status}`);
}
const remoteJson = await response.json();
if (Array.isArray(remoteJson)) {
const item = remoteJson.find((x: any) =>
Number(x.id_canti) === Number(songId) &&
(!x.momenti || (!x.momenti.includes('Playlist') && !x.momenti.includes('UserMetadata')))
);
if (!item) {
throw new Error('Canto non trovato nel profilo remoto.');
}
const id = `remote_share_${item.id_canti}`;
const canto: any = {
id: id,
id_canti: Number(item.id_canti),
titolo: item.titolo || 'Senza Titolo',
autore: item.autore || '',
testo: item.testo || '',
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
link_youtube: item.link_youtube || '',
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || [],
nonValidato: true
};
await this.playlistService.saveRemoteShareCanto(canto);
const toast = await this.toastCtrl.create({
message: `Canto "${canto.titolo}" importato nei brani remoti non validati!`,
duration: 3000,
color: 'success',
position: 'bottom'
});
await toast.present();
await this.router.navigate([], { queryParams: { 'song-uid': null, 'song-id': null }, queryParamsHandling: 'merge', replaceUrl: true });
this.goToCanto(canto.id);
} else {
throw new Error('Formato dati non valido.');
}
} catch (err: any) {
console.error('Failed to import remote song:', err);
const alert = await this.alertCtrl.create({
header: 'Errore Importazione',
message: 'Impossibile scaricare il canto da remoto. Controlla la connessione o il link.',
buttons: ['OK']
});
await alert.present();
await this.router.navigate([], { queryParams: { 'song-uid': null, 'song-id': null }, queryParamsHandling: 'merge', replaceUrl: true });
} finally {
await loading.dismiss();
}
}
async handleRemotePlaylistImport(uid: string, pid?: string) {
const loading = await this.loadingCtrl.create({
message: 'Scaricamento playlist da remoto...'
@@ -657,6 +784,7 @@ export class HomePage implements OnDestroy {
await loading.present();
let hasNavigatedToCanto = false;
const openFirst = this.route.snapshot.queryParams['openFirst'] === '1';
try {
const response = await fetch(`https://api.canticristiani.it/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
if (!response.ok) {
@@ -672,7 +800,9 @@ export class HomePage implements OnDestroy {
id_canti: Number(item.id_canti),
titolo: item.titolo,
testo: item.testo,
accordi: item.testo?.includes('[') ? item.testo : undefined,
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
autore: item.autore || '',
link_youtube: item.link_youtube || '',
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || []
}));
@@ -731,8 +861,10 @@ export class HomePage implements OnDestroy {
if (selectedPl.ids && selectedPl.ids.length > 0) {
hasNavigatedToCanto = true;
await this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null }, queryParamsHandling: 'merge', replaceUrl: true });
this.goToCanto(selectedPl.ids[0]);
await this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null, 'openFirst': null }, queryParamsHandling: 'merge', replaceUrl: true });
if (openFirst) {
this.goToCanto(selectedPl.ids[0]);
}
}
} else {
throw new Error('Formato dati non valido.');
@@ -748,7 +880,7 @@ export class HomePage implements OnDestroy {
await alert.present();
} finally {
if (!hasNavigatedToCanto) {
this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null }, queryParamsHandling: 'merge' });
this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null, 'openFirst': null }, queryParamsHandling: 'merge', replaceUrl: true });
}
}
}
@@ -787,7 +919,9 @@ export class HomePage implements OnDestroy {
id_canti: Number(item.id_canti),
titolo: item.titolo || 'Senza Titolo',
testo: item.testo || '',
accordi: item.testo?.includes('[') ? item.testo : undefined,
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
autore: item.autore || '',
link_youtube: item.link_youtube || '',
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || []
}));
@@ -898,6 +1032,7 @@ export class HomePage implements OnDestroy {
...this.cantiService.canti(),
...this.myCantiService.myCanti(),
...this.playlistService.remoteCustomSongs(),
...this.playlistService.remoteShareCanti(),
...this.comunitaService.comunitaCantiPersonali()
].find(c => c.id === id || String(c.id_canti) === id);
}
@@ -1401,6 +1536,29 @@ export class HomePage implements OnDestroy {
}
}
if (data.includes('import-canto=')) {
try {
let base64 = '';
if (data.includes('?import-canto=')) {
base64 = data.split('?import-canto=')[1];
} else if (data.includes('&import-canto=')) {
base64 = data.split('&import-canto=')[1];
}
if (base64.includes('&')) {
base64 = base64.split('&')[0];
}
if (base64.includes('#')) {
base64 = base64.split('#')[0];
}
this.handleImportCanto(base64);
return;
} catch (e) {
console.error('Failed to parse scanned canto link', e);
}
}
if (data.includes('import=')) {
try {
let base64 = '';
+3
View File
@@ -21,6 +21,9 @@
<ion-button fill="clear" (click)="editOrCloneCanto()" *ngIf="settingsService.showEditor() && !isLandscapeActive()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
<ion-icon slot="icon-only" name="create-outline" color="secondary" style="font-size: 1.3rem;"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="shareCanto()" *ngIf="!isLandscapeActive()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
<ion-icon slot="icon-only" name="share-social-outline" color="secondary" style="font-size: 1.3rem;"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="toggleChords()" *ngIf="!isLandscapeActive()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
<ion-icon slot="icon-only"
[name]="showChords() ? 'musical-notes-outline' : 'text-outline'"
+92 -1
View File
@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, signal, computed, effect, inject, ElementRef, AfterViewInit, untracked, HostListener } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { DomSanitizer, SafeResourceUrl, Meta } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { AlertController, ToastController, GestureController } from '@ionic/angular';
import { CantiService, Canto } from '../../services/canti.service';
@@ -150,6 +150,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
private el = inject(ElementRef);
private sanitizer = inject(DomSanitizer);
public faceDetector = inject(FaceDetectorService);
private meta = inject(Meta);
public enableCameraNavigation = signal<boolean>(false);
@@ -212,6 +213,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
if (!found) {
found = this.playlistService.remoteCustomSongs().find(c => c.id === id || String(c.id_canti) === id);
}
if (!found) {
found = this.playlistService.remoteShareCanti().find(c => c.id === id || String(c.id_canti) === id);
}
if (!found) {
found = comunitaCantiPers.find(c => c.id === id);
}
@@ -237,6 +241,15 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.cantiService.getStorage()?.set('last_song_id', id);
this.channel.postMessage({ type: 'SYNC_CANTO', id: found.id });
this.channel.postMessage({ type: 'SYNC_INDEX', index: 0 });
// Update Open Graph Image tag dynamically
const thumb = this.cantiService.getYoutubeThumb(found.link_youtube);
if (thumb) {
this.meta.updateTag({ property: 'og:image', content: thumb });
} else {
this.meta.updateTag({ property: 'og:image', content: window.location.origin + '/assets/icon/favicon.png' });
}
setTimeout(() => {
const scrollEl = this.el.nativeElement.querySelector('.lyrics-container');
if (scrollEl) {
@@ -1037,6 +1050,84 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
}
async shareCanto() {
const c = this.canto();
if (!c) return;
const isStandard = !c.id.startsWith('my_') && !c.id.startsWith('remote_share_') && !c.nonValidato;
try {
let shareLink = '';
if (isStandard) {
shareLink = `https://www.canticristiani.it/?id=${c.id}`;
} else {
await this.playlistService.syncLocalDataToServer();
const uid = this.settingsService.userUuid();
shareLink = `https://www.canticristiani.it/?song-uid=${uid}&song-id=${c.id_canti}`;
}
let fileToShare: File | null = null;
const thumbUrl = this.cantiService.getYoutubeThumb(c.link_youtube);
// Try to fetch YouTube thumbnail first if available
if (thumbUrl) {
try {
const response = await Promise.race([
fetch(thumbUrl),
new Promise<Response>((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
]);
if (response.ok) {
const blob = await response.blob();
fileToShare = new File([blob], 'song_thumbnail.jpg', { type: 'image/jpeg' });
}
} catch (e) {
console.warn('[Share] Failed to fetch YouTube thumbnail due to CORS or timeout:', e);
}
}
// If no YouTube thumb or fetch failed, fallback to local canticristiani logo (favicon)
if (!fileToShare) {
try {
const response = await fetch('assets/icon/favicon.png');
if (response.ok) {
const blob = await response.blob();
fileToShare = new File([blob], 'canticristiani_logo.png', { type: 'image/png' });
}
} catch (e) {
console.warn('[Share] Failed to fetch local logo:', e);
}
}
const shareDataObj: any = {
title: `Condividi Canto: ${c.titolo}`,
text: isStandard
? `Ecco il canto "${c.titolo}" dall'app Canti Cristiani. Clicca sul link per aprirlo:\n\n`
: `Ecco il canto "${c.titolo}" per l'app Canti Cristiani. Clicca sul link per aggiungerlo:\n\n`,
url: shareLink
};
if (fileToShare && navigator.canShare && navigator.canShare({ files: [fileToShare] })) {
shareDataObj.files = [fileToShare];
}
if (navigator.share) {
await navigator.share(shareDataObj);
} else {
if (navigator.clipboard) {
await navigator.clipboard.writeText(shareLink);
const toast = await this.toastCtrl.create({
message: 'Link di condivisione copiato negli appunti!',
duration: 2500,
color: 'success'
});
await toast.present();
}
}
} catch (err) {
console.error('Failed to share song', err);
}
}
private initPlayer(id: string) {
if (!this.youtubePlayerService.isPlayerSupported()) {
return;
@@ -131,7 +131,8 @@ export class ProposeCantoPage implements OnInit {
const song = [
...this.cantiService.canti(),
...this.myCantiService.myCanti(),
...this.playlistService.remoteCustomSongs()
...this.playlistService.remoteCustomSongs(),
...this.playlistService.remoteShareCanti()
].find(c => c.id === editId);
if (song) {
@@ -961,6 +962,10 @@ export class ProposeCantoPage implements OnInit {
await this.playlistService.replaceSongIdInPlaylists(this.editId, savedCanto.id);
}
if (this.editId && this.editId.startsWith('remote_share_')) {
await this.playlistService.deleteRemoteShareCanto(this.editId);
}
if (this.editId && savedCanto && savedCanto.id && this.editId !== savedCanto.id) {
this.router.navigate(['/player'], { queryParams: { id: savedCanto.id }, replaceUrl: true });
} else {
+6 -2
View File
@@ -156,7 +156,9 @@ export class SettingsPage {
id_canti: Number(item.id_canti),
titolo: item.titolo || 'Senza Titolo',
testo: item.testo || '',
accordi: item.testo?.includes('[') ? item.testo : undefined,
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
autore: item.autore || '',
link_youtube: item.link_youtube || '',
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || []
}));
@@ -342,7 +344,9 @@ export class SettingsPage {
id_canti: Number(item.id_canti),
titolo: item.titolo || 'Senza Titolo',
testo: item.testo || '',
accordi: item.testo?.includes('[') ? item.testo : undefined,
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
autore: item.autore || '',
link_youtube: item.link_youtube || '',
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || []
}));
@@ -0,0 +1,104 @@
import { TestBed } from '@angular/core/testing';
import { LyricsParserService } from './lyrics-parser.service';
import { SettingsService } from './settings.service';
describe('LyricsParserService - Hybrid Notation', () => {
let service: LyricsParserService;
let mockSettingsService: any;
beforeEach(() => {
mockSettingsService = {
chordNotationPreference: jasmine.createSpy('chordNotationPreference').and.returnValue('diesis')
};
TestBed.configureTestingModule({
providers: [
LyricsParserService,
{ provide: SettingsService, useValue: mockSettingsService }
]
});
service = TestBed.inject(LyricsParserService);
});
it('should parse simple text with memorize, replay, copy, paste', () => {
const rawSong = `
{start_verse}{memorize/m1}
Ralle[RE]griamoci, [LA]
non [SOL]c’è spazio alla tri[RE]stezza in questo gior[LA]no [SOL]
{end_verse}
{start_chorus}{copy/rit}
Gloria a [RE]Te Emma[SOL]nue-[LA]le
{end_chorus}
{start_verse}{replay/m1}
Rallegri*amoci,*
Egli *viene a libe*rarci da ogni *male.*
{end_verse}
{start_chorus}{paste/rit}
{end_chorus}
`;
// 1. Test parsing with chords enabled
const sections = service.parseAccordi(rawSong);
expect(sections.length).toBe(4);
// Verse 1 (memorize)
expect(sections[0].type).toBe('verse');
expect(sections[0].lines[0].text).toBe('Rallegriamoci, ');
expect(sections[0].lines[0].segments[0].chord).toBeUndefined();
expect(sections[0].lines[0].segments[1].chord).toBe('RE');
expect(sections[0].lines[0].segments[2].chord).toBe('LA');
// Chorus 1 (copy)
expect(sections[1].type).toBe('chorus');
expect(sections[1].lines[0].text).toBe('Gloria a Te Emmanue-le');
expect(sections[1].lines[0].segments[0].chord).toBeUndefined();
expect(sections[1].lines[0].segments[1].chord).toBe('RE');
expect(sections[1].lines[0].segments[2].chord).toBe('SOL');
expect(sections[1].lines[0].segments[3].chord).toBe('LA');
// Verse 2 (replay)
expect(sections[2].type).toBe('verse');
expect(sections[2].lines[0].text).toBe('Rallegriamoci,');
expect(sections[2].lines[0].segments[0].chord).toBeUndefined();
expect(sections[2].lines[0].segments[1].chord).toBe('RE');
expect(sections[2].lines[0].segments[2].chord).toBe('LA');
expect(sections[2].lines[1].text).toBe('Egli viene a liberarci da ogni male.');
expect(sections[2].lines[1].segments[0].chord).toBeUndefined();
expect(sections[2].lines[1].segments[1].chord).toBe('SOL');
expect(sections[2].lines[1].segments[2].chord).toBe('RE');
expect(sections[2].lines[1].segments[3].chord).toBe('LA');
expect(sections[2].lines[1].segments[4].chord).toBe('SOL');
// Chorus 2 (paste)
expect(sections[3].type).toBe('chorus');
expect(sections[3].lines[0].text).toBe('Gloria a Te Emmanue-le');
expect(sections[3].lines[0].segments[0].chord).toBeUndefined();
expect(sections[3].lines[0].segments[1].chord).toBe('RE');
expect(sections[3].lines[0].segments[2].chord).toBe('SOL');
expect(sections[3].lines[0].segments[3].chord).toBe('LA');
});
it('should strip chords in text-only mode and remove asterisks', () => {
const rawSong = `
{start_verse}{memorize/m1}
Ralle[RE]griamoci, [LA]
{end_verse}
{start_verse}{replay/m1}
Rallegri*amoci,*
{end_verse}
`;
const sections = service.parseText(rawSong);
expect(sections.length).toBe(2);
expect(sections[0].lines[0].text).toBe('Rallegriamoci,');
expect(sections[0].lines[0].segments[0].chord).toBeUndefined();
expect(sections[1].lines[0].text).toBe('Rallegriamoci,');
expect(sections[1].lines[0].segments[0].chord).toBeUndefined();
});
});
+81 -19
View File
@@ -45,36 +45,77 @@ export class LyricsParserService {
let currentLines: ParsedLine[] = [];
let verseNumCounter = 0;
const memories = new Map<string, string[]>();
const copies = new Map<string, string[]>();
let currentAction: { type: 'memorize' | 'replay' | 'copy' | 'paste'; key: string } | null = null;
let currentRawLines: string[] = [];
const lines = raw.split('\n');
const startTagRegex = /^\{(start_verse|start_chorus|start_verse_num|sov|soc)\}(?:\{([a-z]+)\/([a-zA-Z0-9_-]+)\})?$/;
for (const line of lines) {
const trimmed = line.trim();
// Detect section start tags
if (trimmed === '{start_verse}' || trimmed === '{sov}') {
const startMatch = trimmed.match(startTagRegex);
if (startMatch) {
this.pushSection(sections, currentType, currentLines);
currentType = 'verse';
currentLines = [];
continue;
}
if (trimmed === '{start_chorus}' || trimmed === '{soc}') {
this.pushSection(sections, currentType, currentLines);
currentType = 'chorus';
currentLines = [];
continue;
}
if (trimmed === '{start_verse_num}') {
this.pushSection(sections, currentType, currentLines);
currentType = 'verse_num';
verseNumCounter++;
const tag = startMatch[1];
if (tag === 'start_verse' || tag === 'sov') {
currentType = 'verse';
} else if (tag === 'start_chorus' || tag === 'soc') {
currentType = 'chorus';
} else if (tag === 'start_verse_num') {
currentType = 'verse_num';
verseNumCounter++;
}
currentLines = [];
currentRawLines = [];
if (startMatch[2] && startMatch[3]) {
currentAction = {
type: startMatch[2] as any,
key: startMatch[3]
};
if (currentAction.type === 'paste') {
const pastedRawLines = copies.get(currentAction.key) || [];
for (const pLine of pastedRawLines) {
if (withChords) {
currentLines.push(this.parseChordLine(pLine));
} else {
const cleanLine = pLine.replace(/\[[^\]]*\]/g, '').trim();
if (cleanLine.length > 0) {
currentLines.push({
text: cleanLine,
segments: [{ text: cleanLine }]
});
}
}
}
}
} else {
currentAction = null;
}
continue;
}
// Detect section end tags — just skip them
// Detect section end tags
if (trimmed === '{end_verse}' || trimmed === '{eov}' || trimmed === '{end_chorus}' || trimmed === '{eoc}' || trimmed === '{end_verse_num}') {
if (currentAction) {
if (currentAction.type === 'memorize') {
memories.set(currentAction.key, [...currentRawLines]);
} else if (currentAction.type === 'copy') {
copies.set(currentAction.key, [...currentRawLines]);
}
}
this.pushSection(sections, currentType, currentLines, currentType === 'verse_num' ? verseNumCounter : undefined);
currentLines = [];
currentRawLines = [];
currentAction = null;
continue;
}
@@ -88,12 +129,33 @@ export class LyricsParserService {
continue;
}
let resolvedLine = line;
if (currentAction && currentAction.type === 'replay') {
const memLines = memories.get(currentAction.key) || [];
const lineIndex = currentRawLines.length;
if (lineIndex < memLines.length) {
const memLine = memLines[lineIndex];
const chordMatches = [...memLine.matchAll(/\[([^\]]+)\]/g)].map(m => m[0]);
let chordIdx = 0;
resolvedLine = line.replace(/\*/g, () => {
if (chordIdx < chordMatches.length) {
return chordMatches[chordIdx++];
}
return '';
});
} else {
resolvedLine = line.replace(/\*/g, '');
}
}
currentRawLines.push(line);
// Parse line
if (withChords) {
currentLines.push(this.parseChordLine(line));
currentLines.push(this.parseChordLine(resolvedLine));
} else {
// CLEAN CHORDS in text-only mode: remove [anything]
const cleanLine = line.replace(/\[[^\]]*\]/g, '').trim();
const cleanLine = resolvedLine.replace(/\[[^\]]*\]/g, '').trim();
if (cleanLine.length > 0) {
currentLines.push({
text: cleanLine,
+71 -15
View File
@@ -32,6 +32,7 @@ export class PlaylistService {
public remotePlaylist = signal<any | null>(null);
public remoteCustomSongs = signal<any[]>([]);
public remoteShareCanti = signal<Canto[]>([]);
private _storage: Storage | null = null;
private initPromise!: Promise<void>;
@@ -114,6 +115,10 @@ export class PlaylistService {
if (remoteSongs) {
this.remoteCustomSongs.set(remoteSongs);
}
const remoteShareSongs = await this._storage.get('remote_share_canti');
if (remoteShareSongs) {
this.remoteShareCanti.set(remoteShareSongs);
}
// Aggiorna in background la playlist remota per sincronizzare eventuali modifiche
if (remotePl) {
@@ -141,7 +146,9 @@ export class PlaylistService {
id_canti: Number(item.id_canti),
titolo: item.titolo || 'Senza Titolo',
testo: item.testo || '',
accordi: item.testo?.includes('[') ? item.testo : undefined,
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
autore: item.autore || '',
link_youtube: item.link_youtube || '',
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || []
}));
@@ -204,7 +211,7 @@ export class PlaylistService {
});
}
async savePlaylist(name: string, ids: string[], songSettings?: any) {
async savePlaylist(name: string, ids: string[], songSettings?: any, setAsLast = true) {
await this.initPromise;
const key = this.getPlaylistsStorageKey();
const lastKey = `lastPlaylist_${key}`;
@@ -231,11 +238,15 @@ export class PlaylistService {
this.playlists.update(p => [newPlaylist, ...p]);
}
this.lastPlaylist.set(newPlaylist);
if (setAsLast) {
this.lastPlaylist.set(newPlaylist);
}
this.activeListIds.set(ids);
this.activeListName.set(name);
await this._storage?.set(key, this.playlists());
await this._storage?.set(lastKey, newPlaylist);
if (setAsLast) {
await this._storage?.set(lastKey, newPlaylist);
}
// Reset selection mode, IDs and editing state after saving
this.selectedIds.set(new Set());
@@ -345,13 +356,37 @@ export class PlaylistService {
}
try {
const customSongs = this.myCantiService.myCanti().map(c => ({
id_canti: c.id_canti || Date.now(),
titolo: c.titolo || 'Senza Titolo',
momenti: c.id_momenti?.map(id => String(id)) || [],
periodi: [] as string[],
testo: c.testo || ''
}));
const mergedSongsMap = new Map<number, any>();
this.remoteShareCanti().forEach(c => {
const id_canti = c.id_canti || Date.now();
mergedSongsMap.set(id_canti, {
id_canti,
titolo: c.titolo || 'Senza Titolo',
autore: c.autore || '',
link_youtube: c.link_youtube || '',
accordi: c.accordi || '',
momenti: c.id_momenti?.map(id => String(id)) || [],
periodi: [] as string[],
testo: c.testo || ''
});
});
this.myCantiService.myCanti().forEach(c => {
const id_canti = c.id_canti || Date.now();
mergedSongsMap.set(id_canti, {
id_canti,
titolo: c.titolo || 'Senza Titolo',
autore: c.autore || '',
link_youtube: c.link_youtube || '',
accordi: c.accordi || '',
momenti: c.id_momenti?.map(id => String(id)) || [],
periodi: [] as string[],
testo: c.testo || ''
});
});
const customSongs = Array.from(mergedSongsMap.values());
const playlistSongs = this.playlists().map(pl => ({
id_canti: Number(pl.id) || Date.now(),
@@ -451,7 +486,7 @@ export class PlaylistService {
// Always use the production URL for sharing links as requested
const productionUrl = 'https://www.canticristiani.it';
return `${productionUrl}/?import=${base64}`;
return `${productionUrl}/?import=${base64}&openFirst=1`;
}
processImportJson(json: any): boolean {
@@ -460,7 +495,7 @@ export class PlaylistService {
this.activeListIds.set(json.ids);
this.activeListName.set(json.name);
// Automatically save to the device!
this.savePlaylist(json.name, json.ids, json.songSettings);
this.savePlaylist(json.name, json.ids, json.songSettings, false);
return true;
}
return false;
@@ -478,7 +513,7 @@ export class PlaylistService {
{
text: isRemote ? 'Condividi (Sola Lettura)' : 'Sola Lettura (Consultazione)',
handler: () => {
let shareLink = `https://www.canticristiani.it/?playlist-uid=${uid}&playlist-id=${activeId}`;
let shareLink = `https://www.canticristiani.it/?playlist-uid=${uid}&playlist-id=${activeId}&openFirst=1`;
if (isRemote) {
let remoteUid = uid;
let remotePid = activeId;
@@ -487,7 +522,7 @@ export class PlaylistService {
remoteUid = parts[1];
remotePid = parts[2];
}
shareLink = `https://www.canticristiani.it/?playlist-uid=${remoteUid}&playlist-id=${remotePid}`;
shareLink = `https://www.canticristiani.it/?playlist-uid=${remoteUid}&playlist-id=${remotePid}&openFirst=1`;
}
this.executeShare(shareLink, name);
}
@@ -569,4 +604,25 @@ export class PlaylistService {
async loadPlaylists() {
}
async saveRemoteShareCanto(canto: Canto) {
await this.initPromise;
const current = this.remoteShareCanti();
const index = current.findIndex(c => c.id === canto.id);
let updated: Canto[];
if (index > -1) {
updated = current.map(c => c.id === canto.id ? canto : c);
} else {
updated = [...current, canto];
}
this.remoteShareCanti.set(updated);
await this._storage?.set('remote_share_canti', updated);
}
async deleteRemoteShareCanto(id: string) {
await this.initPromise;
const updated = this.remoteShareCanti().filter(c => c.id !== id);
this.remoteShareCanti.set(updated);
await this._storage?.set('remote_share_canti', updated);
}
}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = '2026.06.16.1611';
export const VERSION = '2026.06.17.0049';
+13
View File
@@ -75,6 +75,19 @@
if (bar) bar.style.width = pct + '%';
if (pctText) pctText.textContent = pct + '%';
}
if (options.isRedirect) {
if (bar && bar.parentElement) bar.parentElement.style.display = 'none';
if (pctText) pctText.style.display = 'none';
if (phaseEl) phaseEl.style.display = 'none';
if (versionEl) versionEl.style.display = 'none';
if (titleEl) {
titleEl.textContent = 'Chiudi il browser, è stata aperta la app installata sul device';
titleEl.style.fontSize = '1.4rem';
titleEl.style.lineHeight = '1.5';
}
if (descEl) descEl.style.display = 'none';
}
},
hide: function() {
const loader = document.getElementById('pwa-boot-loader');