-
diff --git a/src/app/pages/propose-canto/propose-canto.page.scss b/src/app/pages/propose-canto/propose-canto.page.scss
index d43f8b9..d5ac634 100644
--- a/src/app/pages/propose-canto/propose-canto.page.scss
+++ b/src/app/pages/propose-canto/propose-canto.page.scss
@@ -113,7 +113,8 @@ body.high-contrast :host ::ng-deep {
}
.propose-container {
- max-width: 800px;
+ max-width: 1400px;
+ width: 100%;
margin: 0 auto;
}
@@ -470,3 +471,233 @@ body.high-contrast :host ::ng-deep {
}
}
}
+
+/* RESPONSIVE LAYOUT & ACTIVE PREVIEW PANE */
+.editor-preview-split {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 24px;
+ align-items: flex-start;
+
+ @media (min-width: 992px) {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+.preview-column {
+ @media (min-width: 992px) {
+ position: sticky;
+ top: 16px;
+ }
+}
+
+.preview-card {
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(10px);
+
+ .preview-header {
+ background: rgba(255, 255, 255, 0.04);
+ padding: 10px 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+
+ .preview-title {
+ font-size: 11px;
+ font-weight: 800;
+ color: var(--ion-color-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .preview-toggle-btn {
+ --color: rgba(255, 255, 255, 0.7);
+ margin: 0;
+ font-size: 11px;
+ font-weight: 600;
+
+ ion-icon {
+ font-size: 14px;
+ }
+ }
+ }
+
+ .preview-body {
+ padding: 24px;
+ min-height: 400px;
+ background: rgba(0, 0, 0, 0.2);
+
+ .preview-song-header {
+ margin-bottom: 24px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ padding-bottom: 14px;
+
+ .preview-song-title {
+ font-size: 1.6rem;
+ font-weight: 700;
+ color: #ffffff;
+ margin: 0 0 6px 0;
+ font-family: 'Outfit', sans-serif;
+ }
+
+ .preview-song-author {
+ font-size: 0.95rem;
+ color: rgba(255, 255, 255, 0.5);
+ margin: 0;
+ font-weight: 500;
+ }
+ }
+
+ .preview-lyrics-container {
+ font-size: 1.1rem;
+ font-family: 'Outfit', sans-serif;
+ overflow-y: auto;
+ max-height: 60vh;
+ padding-right: 8px;
+ scrollbar-width: thin;
+
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 2px;
+ }
+
+ .preview-section {
+ margin-bottom: 1.8rem;
+ position: relative;
+
+ &.chorus {
+ background: rgba(var(--ion-color-secondary-rgb), 0.03);
+ border-left: 3px solid var(--ion-color-secondary);
+ padding-left: 1rem;
+ margin-left: -1rem;
+ border-radius: 0 8px 8px 0;
+ }
+
+ .preview-section-label {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ font-weight: 700;
+ color: var(--ion-color-secondary);
+ margin-bottom: 0.4rem;
+ opacity: 0.7;
+ letter-spacing: 1px;
+ }
+ }
+
+ .preview-lyric-line {
+ margin-bottom: 0.8rem;
+ line-height: 1.6;
+ color: rgba(255, 255, 255, 0.9);
+ min-height: 1.5em;
+ }
+
+ .preview-chord-segment {
+ display: inline-flex;
+ flex-direction: column;
+ vertical-align: bottom;
+ margin-right: 0.25em;
+
+ &.contiguous-next {
+ margin-right: 0 !important;
+ }
+
+ .preview-chord {
+ font-size: 0.78em;
+ font-weight: 700;
+ color: var(--ion-color-secondary);
+ height: 1.25em;
+ margin-bottom: -0.2em;
+ }
+
+ .preview-seg-text {
+ white-space: pre;
+ &::after {
+ content: '\200b';
+ }
+ }
+ }
+
+ .preview-empty {
+ color: rgba(255, 255, 255, 0.3);
+ text-align: center;
+ padding-top: 80px;
+ font-style: italic;
+ font-size: 0.95rem;
+ }
+ }
+ }
+
+ /* High Contrast mode overrides */
+ &.hc {
+ background: #ffffff;
+ border: 2px solid #000000;
+ box-shadow: none;
+
+ .preview-header {
+ background: #f0f0f0;
+ border-bottom: 2px solid #000000;
+
+ .preview-title {
+ color: #000000;
+ }
+
+ .preview-toggle-btn {
+ --color: #000000;
+ font-weight: 700;
+ }
+ }
+
+ .preview-body {
+ background: #ffffff;
+
+ .preview-song-header {
+ border-bottom: 2px solid #000000;
+
+ .preview-song-title {
+ color: #000000;
+ }
+
+ .preview-song-author {
+ color: #333333;
+ }
+ }
+
+ .preview-lyrics-container {
+ .preview-section {
+ &.chorus {
+ background: #f5f5f5;
+ border-left: 3px solid #000000;
+ }
+
+ .preview-section-label {
+ color: #000000;
+ }
+ }
+
+ .preview-lyric-line {
+ color: #000000;
+ }
+
+ .preview-chord-segment {
+ .preview-chord {
+ color: #000000;
+ text-decoration: underline;
+ font-weight: 800;
+ }
+ }
+
+ .preview-empty {
+ color: #666666;
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/pages/propose-canto/propose-canto.page.ts b/src/app/pages/propose-canto/propose-canto.page.ts
index 3ca3536..c1ff722 100644
--- a/src/app/pages/propose-canto/propose-canto.page.ts
+++ b/src/app/pages/propose-canto/propose-canto.page.ts
@@ -8,6 +8,7 @@ import { MyCantiService } from '../../services/my-canti.service';
import { PlaylistService } from '../../services/playlist.service';
import { ThemeService } from '../../services/theme.service';
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
+import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service';
@Component({
selector: 'app-propose-canto',
@@ -27,6 +28,17 @@ export class ProposeCantoPage implements OnInit {
public themeService = inject(ThemeService);
private route = inject(ActivatedRoute);
private router = inject(Router);
+ public lyricsParser = inject(LyricsParserService);
+
+ showChordsPreview: boolean = true;
+
+ get parsedSections(): ParsedSection[] {
+ return this.lyricsParser.parseAccordi(this.content);
+ }
+
+ toggleChordsPreview() {
+ this.showChordsPreview = !this.showChordsPreview;
+ }
title: string = '';
author: string = '';
diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html
index 42a2be6..a3267df 100644
--- a/src/app/pages/settings/settings.page.html
+++ b/src/app/pages/settings/settings.page.html
@@ -62,14 +62,7 @@
-
-
-
- Nascondi Barre (Player)
- Modalità immersiva nel dettaglio canto
-
-
-
+
diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts
index 3c633f0..4a32a7a 100644
--- a/src/app/pages/settings/settings.page.ts
+++ b/src/app/pages/settings/settings.page.ts
@@ -347,15 +347,33 @@ export class SettingsPage {
await registration.update();
}
+ let sub: any = null;
+ let readyPromise: Promise | undefined = undefined;
+
+ if (this.swUpdate.isEnabled) {
+ readyPromise = new Promise((resolve) => {
+ sub = this.swUpdate.versionUpdates
+ .pipe(
+ filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
+ first()
+ )
+ .subscribe(() => {
+ resolve();
+ });
+ });
+ }
+
// Layer 2: Ask Angular SW to check
if (this.swUpdate.isEnabled) {
const swFoundUpdate = await this.swUpdate.checkForUpdate();
if (swFoundUpdate) {
- await this.applyUpdateAndReload();
+ await this.applyUpdateAndReload(readyPromise, sub);
return true;
}
}
+ if (sub) sub.unsubscribe();
+
// Layer 3: Fallback — check version.json
const versionMismatch = await this.checkVersionJson();
if (versionMismatch) {
@@ -377,7 +395,7 @@ export class SettingsPage {
}
}
- private async applyUpdateAndReload() {
+ private async applyUpdateAndReload(readyPromise?: Promise, subscription?: any) {
const overlay = showFullscreenUpdateOverlay();
let activated = false;
@@ -397,24 +415,19 @@ export class SettingsPage {
}, 600);
};
- // Listen for VERSION_READY + activate + reload
- if (this.swUpdate.isEnabled) {
- this.swUpdate.versionUpdates
- .pipe(
- filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
- first()
- )
- .subscribe(() => {
- console.log('[PWA-Update] Settings: version ready, activating...');
- activateAndReload();
- });
+ if (readyPromise) {
+ Promise.race([
+ readyPromise,
+ new Promise((resolve) => setTimeout(resolve, 25000))
+ ]).then(() => {
+ if (subscription) subscription.unsubscribe();
+ activateAndReload();
+ });
+ } else {
+ setTimeout(() => {
+ activateAndReload();
+ }, 1000);
}
-
- // Safety timeout: reload after 6s regardless
- setTimeout(() => {
- console.log('[PWA-Update] Settings: safety timeout reached, activating...');
- activateAndReload();
- }, 6000);
}
private async checkVersionJson(): Promise {
diff --git a/src/app/services/canti.service.ts b/src/app/services/canti.service.ts
index 6c93230..afb7ead 100644
--- a/src/app/services/canti.service.ts
+++ b/src/app/services/canti.service.ts
@@ -1,6 +1,7 @@
import { Injectable, signal, inject } from '@angular/core';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { Storage } from '@ionic/storage-angular';
+import { SettingsService } from './settings.service';
export interface Canto {
id: string;
@@ -34,6 +35,7 @@ export interface CantoEseguito {
export class CantiService {
private http = inject(HttpClient);
private storage = inject(Storage);
+ private settingsService = inject(SettingsService);
private _storage: Storage | null = null;
public canti = signal([]);
@@ -58,6 +60,20 @@ export class CantiService {
await this.loadFromStorage();
if (this.canti() && this.canti().length > 0) {
this.firstLoadCompleted.set(true);
+ if (this.settingsService.isVersionCheckComplete() && (window as any).PwaLoader) {
+ (window as any).PwaLoader.hide();
+ }
+ } else {
+ // First boot or data cleared: show setup loader immediately
+ if ((window as any).PwaLoader) {
+ (window as any).PwaLoader.show();
+ (window as any).PwaLoader.update({
+ title: 'Setup in corso',
+ phase: 'Fase: Setup',
+ desc: 'Configurazione iniziale e caricamento canti...',
+ percent: 0
+ });
+ }
}
this.refresh();
}
@@ -85,10 +101,16 @@ export class CantiService {
}).subscribe({
next: async (event: any) => {
if (event.type === HttpEventType.DownloadProgress) {
+ let pct = 0;
if (event.total) {
- this.progress.set(Math.round((event.loaded / event.total) * 100));
+ pct = Math.round((event.loaded / event.total) * 100);
+ this.progress.set(pct);
} else {
this.progress.update(p => p < 90 ? p + 5 : p);
+ pct = this.progress();
+ }
+ if (!this.firstLoadCompleted() && (window as any).PwaLoader) {
+ (window as any).PwaLoader.update({ percent: pct });
}
} else if (event.type === HttpEventType.Response) {
const response = event.body;
@@ -137,12 +159,19 @@ export class CantiService {
this.progress.set(100);
this.loading.set(false);
this.firstLoadCompleted.set(true);
+
+ if (this.settingsService.isVersionCheckComplete() && (window as any).PwaLoader) {
+ (window as any).PwaLoader.hide();
+ }
}
},
error: (error) => {
console.error('Failed to fetch canti', error);
this.loading.set(false);
this.firstLoadCompleted.set(true);
+ if ((window as any).PwaLoader) {
+ (window as any).PwaLoader.hide();
+ }
}
});
}
diff --git a/src/app/services/lyrics-parser.service.ts b/src/app/services/lyrics-parser.service.ts
index 5a930ce..5af9c5b 100644
--- a/src/app/services/lyrics-parser.service.ts
+++ b/src/app/services/lyrics-parser.service.ts
@@ -149,7 +149,7 @@ export class LyricsParserService {
}
// Add the chord as a new segment (text will be filled by next text chunk)
- segments.push({ chord: match[1], text: '' });
+ segments.push({ chord: this.transposeChord(match[1], 0), text: '' });
lastIndex = match.index + match[0].length;
}
@@ -197,7 +197,7 @@ export class LyricsParserService {
const upperChord = chord.toUpperCase();
for (const r of possibleRoots) {
- if (upperChord.startsWith(r)) {
+ if (upperChord.startsWith(r.toUpperCase())) {
root = r;
suffix = chord.substring(r.length);
break;
@@ -206,8 +206,11 @@ export class LyricsParserService {
if (!root) return chord;
- let index = this.scale.indexOf(root);
- if (index === -1) index = this.flatScale.indexOf(root);
+ const upperRoot = root.toUpperCase();
+ let index = this.scale.indexOf(upperRoot);
+ if (index === -1) {
+ index = this.flatScale.findIndex(n => n.toUpperCase() === upperRoot);
+ }
if (index === -1) return chord;
let newIndex = (index + semitones) % 12;
@@ -231,7 +234,12 @@ export class LyricsParserService {
}
}
- const newRoot = useFlat ? this.flatScale[newIndex] : this.scale[newIndex];
+ let newRoot = useFlat ? this.flatScale[newIndex] : this.scale[newIndex];
+ if (newRoot === 'LA#') {
+ newRoot = 'SIb';
+ } else if (newRoot === 'RE#') {
+ newRoot = 'MIb';
+ }
return newRoot + suffix;
}
@@ -251,4 +259,35 @@ export class LyricsParserService {
}))
}));
}
+
+ /**
+ * Helper to check if the segment at the given index is contiguous with the next segment.
+ * This means the text is split within a word (e.g. by a chord tag like "rima[RE]ne").
+ */
+ isContiguousNext(segments: ChordSegment[], index: number): boolean {
+ if (!segments || index >= segments.length - 1) return false;
+
+ // Find the next segment with non-empty text
+ let nextWithText: ChordSegment | null = null;
+ for (let i = index + 1; i < segments.length; i++) {
+ if (segments[i].text && segments[i].text.length > 0) {
+ nextWithText = segments[i];
+ break;
+ }
+ }
+
+ const currentText = segments[index].text || '';
+ if (!currentText) {
+ // If current segment has no text, it should have no margin-right to not add extra spacing
+ return true;
+ }
+
+ if (!nextWithText) return false;
+
+ const endsWithNonSpace = !/\s$/.test(currentText);
+ const startsWithNonSpace = !/^\s/.test(nextWithText.text);
+
+ return endsWithNonSpace && startsWithNonSpace;
+ }
}
+
diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts
index c991d88..def04a3 100644
--- a/src/app/services/settings.service.ts
+++ b/src/app/services/settings.service.ts
@@ -64,6 +64,7 @@ export class SettingsService {
public isStandalone = signal(false);
public isIos = signal(false);
public isAndroid = signal(false);
+ public isVersionCheckComplete = signal(false);
constructor() {
// Gestione/Generazione ID utente univoco
diff --git a/src/app/services/youtube-player.service.ts b/src/app/services/youtube-player.service.ts
index 5ca3b67..3ebaa37 100644
--- a/src/app/services/youtube-player.service.ts
+++ b/src/app/services/youtube-player.service.ts
@@ -22,12 +22,6 @@ export class YoutubePlayerService {
if (!this.connectivityService.isOnline()) {
return false;
}
- // Check if iPhone/iPad/iPod
- const userAgent = window.navigator.userAgent.toLowerCase();
- const isIOS = /iphone|ipad|ipod/.test(userAgent);
- if (isIOS) {
- return false;
- }
return true;
});
diff --git a/src/app/version.ts b/src/app/version.ts
index 2a45f40..ef20d01 100644
--- a/src/app/version.ts
+++ b/src/app/version.ts
@@ -1 +1 @@
-export const VERSION = '2026.06.12.1712';
+export const VERSION = '2026.06.14.1936';
diff --git a/src/index.html b/src/index.html
index 380f9d0..56ad07e 100644
--- a/src/index.html
+++ b/src/index.html
@@ -34,77 +34,155 @@
-
-
-
-
-

-
-
Avvio in corso
-
Caricamento dei componenti dell'applicazione...
-
Ricerca versione...
-
Fase: Avvio
-
-
0%
+
+
+
+
+
Avvio in corso
+
Caricamento dei componenti dell'applicazione...
+
Ricerca versione...
+
Fase: Avvio
+
+
0%
-
-
+
+
+
+
+