tasto back e bugfix editor
This commit is contained in:
@@ -3,10 +3,11 @@ import { ThemeService } from './services/theme.service';
|
|||||||
import { CantiService } from './services/canti.service';
|
import { CantiService } from './services/canti.service';
|
||||||
import { SettingsService } from './services/settings.service';
|
import { SettingsService } from './services/settings.service';
|
||||||
import { VERSION } from './version';
|
import { VERSION } from './version';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
|
||||||
import { ToastController } from '@ionic/angular';
|
import { ToastController, Platform } from '@ionic/angular';
|
||||||
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
||||||
import { filter, first } from 'rxjs/operators';
|
import { filter, first } from 'rxjs/operators';
|
||||||
|
import { App } from '@capacitor/app';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -19,6 +20,7 @@ export class AppComponent implements OnInit {
|
|||||||
public cantiService = inject(CantiService);
|
public cantiService = inject(CantiService);
|
||||||
public settingsService = inject(SettingsService);
|
public settingsService = inject(SettingsService);
|
||||||
public version = VERSION;
|
public version = VERSION;
|
||||||
|
private platform = inject(Platform);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
private toastCtrl = inject(ToastController);
|
private toastCtrl = inject(ToastController);
|
||||||
@@ -60,6 +62,27 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// Gestione tasto back per PWA/Browser (intercettando popstate di Angular Router)
|
||||||
|
this.router.events.subscribe(event => {
|
||||||
|
if (event instanceof NavigationStart && event.navigationTrigger === 'popstate') {
|
||||||
|
const targetUrl = event.url.split('?')[0];
|
||||||
|
if (targetUrl !== '/home' && targetUrl !== '/') {
|
||||||
|
this.router.navigate(['/home'], { replaceUrl: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestione tasto back hardware per nativo (Capacitor/Cordova)
|
||||||
|
this.platform.backButton.subscribeWithPriority(9999, () => {
|
||||||
|
const currentUrl = this.router.url;
|
||||||
|
const path = currentUrl.split('?')[0];
|
||||||
|
if (path !== '/home' && path !== '/') {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
} else {
|
||||||
|
App.exitApp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add global horizontal scroll support for wheel on horizontal containers
|
// Add global horizontal scroll support for wheel on horizontal containers
|
||||||
window.addEventListener('wheel', (event: WheelEvent) => {
|
window.addEventListener('wheel', (event: WheelEvent) => {
|
||||||
if (Math.abs(event.deltaY) > 0 && Math.abs(event.deltaX) === 0) {
|
if (Math.abs(event.deltaY) > 0 && Math.abs(event.deltaX) === 0) {
|
||||||
|
|||||||
@@ -139,6 +139,13 @@
|
|||||||
<!-- Selection Mode Actions -->
|
<!-- Selection Mode Actions -->
|
||||||
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
|
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
|
||||||
<span class="selection-count">{{ reorderList().length }}</span>
|
<span class="selection-count">{{ reorderList().length }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[value]="playlistService.activeListName() || ''"
|
||||||
|
(input)="playlistService.activeListName.set($any($event.target).value)"
|
||||||
|
placeholder="Nome playlist..."
|
||||||
|
class="playlist-name-input outfit-font"
|
||||||
|
/>
|
||||||
<ion-button fill="clear" color="secondary" (click)="toggleAddingSongs()" class="mini-action-btn" *ngIf="reorderList().length > 0">
|
<ion-button fill="clear" color="secondary" (click)="toggleAddingSongs()" class="mini-action-btn" *ngIf="reorderList().length > 0">
|
||||||
<ion-icon slot="icon-only" [name]="isAddingSongs() ? 'list-outline' : 'add-circle-outline'"></ion-icon>
|
<ion-icon slot="icon-only" [name]="isAddingSongs() ? 'list-outline' : 'add-circle-outline'"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -155,6 +162,9 @@
|
|||||||
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn" *ngIf="!isComunitaPlaylist() && isActivePlaylistSaved()">
|
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn" *ngIf="!isComunitaPlaylist() && isActivePlaylistSaved()">
|
||||||
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<ion-button fill="clear" color="secondary" (click)="cloneActivePlaylist()" class="mini-action-btn" title="Clona Playlist">
|
||||||
|
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
<ion-button fill="clear" color="secondary" (click)="shareActivePlaylist()" class="mini-action-btn">
|
<ion-button fill="clear" color="secondary" (click)="shareActivePlaylist()" class="mini-action-btn">
|
||||||
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -383,6 +383,12 @@ ion-title {
|
|||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
background: rgba(255, 255, 255, 0.2) !important;
|
background: rgba(255, 255, 255, 0.2) !important;
|
||||||
}
|
}
|
||||||
|
.playlist-name-input {
|
||||||
|
color: #000000 !important;
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom Item Layout
|
// Custom Item Layout
|
||||||
@@ -676,6 +682,30 @@ ion-title {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playlist-name-input {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
margin: 0 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
width: 120px;
|
||||||
|
height: 24px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--ion-color-secondary);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 0 6px rgba(var(--ion-color-secondary-rgb), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-action-btn {
|
.small-action-btn {
|
||||||
|
|||||||
@@ -1778,6 +1778,50 @@ export class HomePage implements OnDestroy {
|
|||||||
this.playlistService.sharePlaylistQR(ids, name, songSettings);
|
this.playlistService.sharePlaylistQR(ids, name, songSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cloneActivePlaylist() {
|
||||||
|
const id = this.playlistService.activePlaylistId();
|
||||||
|
const currentName = this.playlistService.activeListName() || 'Playlist';
|
||||||
|
const activeIds = this.playlistService.activeListIds();
|
||||||
|
|
||||||
|
const allPls = this.playlistService.allPlaylists();
|
||||||
|
const activePl = allPls.find(p => p.id === id);
|
||||||
|
const songSettings = activePl ? activePl.songSettings : undefined;
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Clona Playlist',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Nome della nuova playlist',
|
||||||
|
value: `${currentName} (Copia)`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Clona',
|
||||||
|
handler: async (data) => {
|
||||||
|
if (data.name) {
|
||||||
|
await this.playlistService.clonePlaylist(data.name, [...activeIds], songSettings);
|
||||||
|
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: `Playlist clonata nel profilo: ${data.name}`,
|
||||||
|
duration: 2500,
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
async importPlaylist() {
|
async importPlaylist() {
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
component: QrScannerComponent
|
component: QrScannerComponent
|
||||||
|
|||||||
@@ -31,9 +31,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Segment Control for mobile layout -->
|
||||||
|
<ion-segment [(ngModel)]="activeTab" class="custom-mobile-segment ion-margin-bottom" mode="md">
|
||||||
|
<ion-segment-button value="editor">
|
||||||
|
<ion-label>Editor</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="preview">
|
||||||
|
<ion-label>Anteprima</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
</ion-segment>
|
||||||
|
|
||||||
<div class="editor-preview-split">
|
<div class="editor-preview-split">
|
||||||
<!-- Left Column: Inputs & Editor -->
|
<!-- Left Column: Inputs & Editor -->
|
||||||
<div class="editor-column">
|
<div class="editor-column" [class.hide-on-mobile]="activeTab === 'preview'">
|
||||||
<ion-list lines="none" class="input-list">
|
<ion-list lines="none" class="input-list">
|
||||||
<ion-item class="custom-input-item">
|
<ion-item class="custom-input-item">
|
||||||
<ion-label position="stacked">Titolo del Canto</ion-label>
|
<ion-label position="stacked">Titolo del Canto</ion-label>
|
||||||
@@ -44,7 +54,7 @@
|
|||||||
<ion-item class="custom-input-item select-item">
|
<ion-item class="custom-input-item select-item">
|
||||||
<ion-label position="stacked">Momento Liturgico</ion-label>
|
<ion-label position="stacked">Momento Liturgico</ion-label>
|
||||||
<ion-select [(ngModel)]="selectedLiturgico" placeholder="Scegli momento" multiple="true" interface="popover">
|
<ion-select [(ngModel)]="selectedLiturgico" placeholder="Scegli momento" multiple="true" interface="popover">
|
||||||
<ion-select-option *ngFor="let lit of cantiService.indiceLiturgico()" [value]="lit.id">
|
<ion-select-option *ngFor="let lit of sortedLiturgico" [value]="lit.id">
|
||||||
{{ lit.tag_name }}
|
{{ lit.tag_name }}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
@@ -53,7 +63,7 @@
|
|||||||
<ion-item class="custom-input-item select-item">
|
<ion-item class="custom-input-item select-item">
|
||||||
<ion-label position="stacked">Periodo / Tema</ion-label>
|
<ion-label position="stacked">Periodo / Tema</ion-label>
|
||||||
<ion-select [(ngModel)]="selectedTematico" placeholder="Scegli tema" multiple="true" interface="popover">
|
<ion-select [(ngModel)]="selectedTematico" placeholder="Scegli tema" multiple="true" interface="popover">
|
||||||
<ion-select-option *ngFor="let tem of cantiService.indiceTematico()" [value]="tem.id">
|
<ion-select-option *ngFor="let tem of sortedTematico" [value]="tem.id">
|
||||||
{{ tem.tag_name }}
|
{{ tem.tag_name }}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
@@ -103,13 +113,16 @@
|
|||||||
<span>Editor Testo</span>
|
<span>Editor Testo</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0">
|
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0" title="Annulla ultima modifica">
|
||||||
<ion-icon name="undo-outline"></ion-icon>
|
<ion-icon name="undo-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" size="small" (click)="takePhoto()">
|
<ion-button fill="clear" size="small" (click)="deduceChords()" [disabled]="!content" title="Deduci accordi per le altre strofe">
|
||||||
|
<ion-icon name="musical-notes-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" size="small" (click)="takePhoto()" title="Scatta foto">
|
||||||
<ion-icon name="camera-outline"></ion-icon>
|
<ion-icon name="camera-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" size="small" (click)="chooseFile()">
|
<ion-button fill="clear" size="small" (click)="chooseFile()" title="Allega file o PDF">
|
||||||
<ion-icon name="document-attach-outline"></ion-icon>
|
<ion-icon name="document-attach-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Active Preview Pane -->
|
<!-- Right Column: Active Preview Pane -->
|
||||||
<div class="preview-column">
|
<div class="preview-column" [class.hide-on-mobile]="activeTab === 'editor'">
|
||||||
<div class="preview-card" [class.hc]="isHighContrast">
|
<div class="preview-card" [class.hc]="isHighContrast">
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<span class="preview-title">Anteprima Attiva</span>
|
<span class="preview-title">Anteprima Attiva</span>
|
||||||
|
|||||||
@@ -702,3 +702,90 @@ body.high-contrast :host ::ng-deep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CUSTOM STYLES FOR INTUITIVE MOBILE RESPONSIVENESS */
|
||||||
|
.custom-mobile-segment {
|
||||||
|
display: none;
|
||||||
|
--background: rgba(255, 255, 255, 0.05);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
ion-segment-button {
|
||||||
|
--color: rgba(255, 255, 255, 0.6);
|
||||||
|
--color-checked: #ffffff;
|
||||||
|
--indicator-color: var(--ion-color-secondary);
|
||||||
|
--border-radius: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.high-contrast :host ::ng-deep {
|
||||||
|
.custom-mobile-segment {
|
||||||
|
--background: #f0f0f0 !important;
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
border: 2px solid #000000 !important;
|
||||||
|
|
||||||
|
ion-segment-button {
|
||||||
|
--color: #333333 !important;
|
||||||
|
--color-checked: #000000 !important;
|
||||||
|
--indicator-color: #000000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.custom-mobile-segment {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-on-mobile {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-x: visible;
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
:host ::ng-deep .category-selectors {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper {
|
||||||
|
.editor-header {
|
||||||
|
padding: 6px 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.editor-title-group {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
private alertCtrl = inject(AlertController);
|
private alertCtrl = inject(AlertController);
|
||||||
|
|
||||||
showChordsPreview: boolean = true;
|
showChordsPreview: boolean = true;
|
||||||
|
activeTab: string = 'editor';
|
||||||
|
|
||||||
get parsedSections(): ParsedSection[] {
|
get parsedSections(): ParsedSection[] {
|
||||||
return this.lyricsParser.parseAccordi(this.content);
|
return this.lyricsParser.parseAccordi(this.content);
|
||||||
@@ -47,6 +48,15 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
youtubeLink: string = '';
|
youtubeLink: string = '';
|
||||||
selectedLiturgico: number[] = [];
|
selectedLiturgico: number[] = [];
|
||||||
selectedTematico: number[] = [];
|
selectedTematico: number[] = [];
|
||||||
|
|
||||||
|
get sortedLiturgico() {
|
||||||
|
return [...this.cantiService.indiceLiturgico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
get sortedTematico() {
|
||||||
|
return [...this.cantiService.indiceTematico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
|
||||||
|
}
|
||||||
|
|
||||||
editId: string | null = null;
|
editId: string | null = null;
|
||||||
|
|
||||||
private _content: string = '';
|
private _content: string = '';
|
||||||
@@ -195,6 +205,194 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
this.fileInput.nativeElement.click();
|
this.fileInput.nativeElement.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deduceChords() {
|
||||||
|
if (!this.content) return;
|
||||||
|
|
||||||
|
// Salva nello stack degli undo
|
||||||
|
this.saveToUndoStack(this.content);
|
||||||
|
|
||||||
|
const lines = this.content.split('\n');
|
||||||
|
|
||||||
|
interface SectionLine {
|
||||||
|
originalIndex: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Section {
|
||||||
|
type: 'verse' | 'chorus' | 'other';
|
||||||
|
startTag: string | null;
|
||||||
|
endTag: string | null;
|
||||||
|
lines: SectionLine[];
|
||||||
|
hasChords: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections: Section[] = [];
|
||||||
|
let currentSection: Section = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false };
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
const startMatch = trimmed.match(/^\{(start_verse|start_chorus|start_verse_num|sov|soc)\}/);
|
||||||
|
const endMatch = trimmed === '{end_verse}' || trimmed === '{eov}' || trimmed === '{end_chorus}' || trimmed === '{eoc}' || trimmed === '{end_verse_num}';
|
||||||
|
|
||||||
|
if (startMatch) {
|
||||||
|
if (currentSection.lines.length > 0 || currentSection.startTag) {
|
||||||
|
sections.push(currentSection);
|
||||||
|
}
|
||||||
|
let type: 'verse' | 'chorus' | 'other' = 'other';
|
||||||
|
const tag = startMatch[1];
|
||||||
|
if (tag === 'start_verse' || tag === 'start_verse_num' || tag === 'sov') {
|
||||||
|
type = 'verse';
|
||||||
|
} else if (tag === 'start_chorus' || tag === 'soc') {
|
||||||
|
type = 'chorus';
|
||||||
|
}
|
||||||
|
currentSection = { type, startTag: line, endTag: null, lines: [], hasChords: false };
|
||||||
|
} else if (endMatch) {
|
||||||
|
currentSection.endTag = line;
|
||||||
|
sections.push(currentSection);
|
||||||
|
currentSection = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false };
|
||||||
|
} else {
|
||||||
|
if (trimmed === '' && !currentSection.startTag) {
|
||||||
|
if (currentSection.lines.length > 0) {
|
||||||
|
sections.push(currentSection);
|
||||||
|
}
|
||||||
|
sections.push({ type: 'other', startTag: null, endTag: null, lines: [{ originalIndex: i, text: '' }], hasChords: false });
|
||||||
|
currentSection = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false };
|
||||||
|
} else {
|
||||||
|
const hasChords = /\[[^\]]+\]/.test(line);
|
||||||
|
if (hasChords) {
|
||||||
|
currentSection.hasChords = true;
|
||||||
|
}
|
||||||
|
if (currentSection.lines.length === 0 && !currentSection.startTag && trimmed !== '') {
|
||||||
|
currentSection.type = 'verse';
|
||||||
|
}
|
||||||
|
const isComment = trimmed.startsWith('{c:') || trimmed.startsWith('{comment:');
|
||||||
|
if (!isComment) {
|
||||||
|
currentSection.lines.push({ originalIndex: i, text: line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentSection.lines.length > 0 || currentSection.startTag) {
|
||||||
|
sections.push(currentSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateVerse = sections.find(s => s.type === 'verse' && s.hasChords);
|
||||||
|
const templateChorus = sections.find(s => s.type === 'chorus' && s.hasChords);
|
||||||
|
|
||||||
|
let deducedCount = 0;
|
||||||
|
|
||||||
|
const getWords = (text: string) => {
|
||||||
|
const words: { text: string; start: number; end: number }[] = [];
|
||||||
|
const regex = /\S+/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
words.push({
|
||||||
|
text: match[0],
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return words;
|
||||||
|
};
|
||||||
|
|
||||||
|
const alignChords = (templateLine: string, targetLine: string): string => {
|
||||||
|
if (/\[[^\]]+\]/.test(targetLine)) {
|
||||||
|
return targetLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedTemplate = this.lyricsParser.parseChordLine(templateLine);
|
||||||
|
const templateClean = parsedTemplate.text;
|
||||||
|
const targetClean = targetLine.replace(/\[[^\]]*\]/g, '');
|
||||||
|
|
||||||
|
if (!templateClean.trim() || !targetClean.trim()) {
|
||||||
|
return targetLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chords: { chord: string; charIndex: number }[] = [];
|
||||||
|
let charAcc = 0;
|
||||||
|
parsedTemplate.segments.forEach(seg => {
|
||||||
|
if (seg.chord) {
|
||||||
|
chords.push({ chord: seg.chord, charIndex: charAcc });
|
||||||
|
}
|
||||||
|
charAcc += seg.text.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chords.length === 0) {
|
||||||
|
return targetLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateWords = getWords(templateClean);
|
||||||
|
const targetWords = getWords(targetClean);
|
||||||
|
|
||||||
|
const insertions: { chord: string; index: number }[] = [];
|
||||||
|
|
||||||
|
chords.forEach(c => {
|
||||||
|
const ratio = c.charIndex / Math.max(1, templateClean.length);
|
||||||
|
const wordIdx = templateWords.findIndex(w => Math.abs(w.start - c.charIndex) <= 1);
|
||||||
|
|
||||||
|
let targetIndex = 0;
|
||||||
|
if (wordIdx !== -1 && templateWords.length > 1 && targetWords.length > 1) {
|
||||||
|
const wRatio = wordIdx / (templateWords.length - 1);
|
||||||
|
const targetWIdx = Math.round(wRatio * (targetWords.length - 1));
|
||||||
|
targetIndex = targetWords[targetWIdx].start;
|
||||||
|
} else {
|
||||||
|
targetIndex = Math.round(ratio * targetClean.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
insertions.push({ chord: c.chord, index: targetIndex });
|
||||||
|
});
|
||||||
|
|
||||||
|
insertions.sort((a, b) => b.index - a.index);
|
||||||
|
|
||||||
|
let result = targetClean;
|
||||||
|
insertions.forEach(ins => {
|
||||||
|
result = result.substring(0, ins.index) + `[${ins.chord}]` + result.substring(ins.index);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newLines = [...lines];
|
||||||
|
|
||||||
|
sections.forEach(sec => {
|
||||||
|
if (!sec.hasChords && sec.type !== 'other') {
|
||||||
|
const template = sec.type === 'verse' ? templateVerse : templateChorus;
|
||||||
|
if (template) {
|
||||||
|
for (let j = 0; j < sec.lines.length; j++) {
|
||||||
|
const targetSecLine = sec.lines[j];
|
||||||
|
if (j < template.lines.length) {
|
||||||
|
const templateSecLine = template.lines[j];
|
||||||
|
const aligned = alignChords(templateSecLine.text, targetSecLine.text);
|
||||||
|
if (aligned !== targetSecLine.text) {
|
||||||
|
newLines[targetSecLine.originalIndex] = aligned;
|
||||||
|
deducedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deducedCount > 0) {
|
||||||
|
this.content = newLines.join('\n');
|
||||||
|
const toast = await this.toastController.create({
|
||||||
|
message: `Accordi dedotti con successo in ${deducedCount} righe!`,
|
||||||
|
duration: 3000,
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
toast.present();
|
||||||
|
} else {
|
||||||
|
const toast = await this.toastController.create({
|
||||||
|
message: 'Nessuna strofa compatibile trovata o accordi già presenti.',
|
||||||
|
duration: 3000,
|
||||||
|
color: 'warning'
|
||||||
|
});
|
||||||
|
toast.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDragOver(event: DragEvent) {
|
onDragOver(event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -804,11 +1002,11 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
return italianChordsCount > englishChordsCount;
|
return italianChordsCount > englishChordsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
isChordWord(text: string, isItalian: boolean): boolean {
|
isChordWord(text: string, isItalian: boolean, allowLowercase: boolean = false): boolean {
|
||||||
if (!text || text.length === 0) return false;
|
if (!text || text.length === 0) return false;
|
||||||
const lower = text.toLowerCase();
|
const lower = text.toLowerCase();
|
||||||
// Skip common valid words written purely in lowercase
|
// Skip common valid words written purely in lowercase unless allowLowercase is true
|
||||||
if (text[0] === lower[0] && ['la', 'mi', 're', 'do', 'si', 'fa', 'e'].includes(lower)) {
|
if (!allowLowercase && text[0] === lower[0] && ['la', 'mi', 're', 'do', 'si', 'fa', 'e'].includes(lower)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let clean = text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
let clean = text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
||||||
@@ -943,7 +1141,7 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
let hasLongNonChord = false;
|
let hasLongNonChord = false;
|
||||||
|
|
||||||
line.forEach(w => {
|
line.forEach(w => {
|
||||||
if (this.isChordWord(w.text, isItalian)) {
|
if (this.isChordWord(w.text, isItalian, true)) {
|
||||||
chordCount++;
|
chordCount++;
|
||||||
} else {
|
} else {
|
||||||
const clean = w.text.replace(/[.,:;!\?]/g, '').trim();
|
const clean = w.text.replace(/[.,:;!\?]/g, '').trim();
|
||||||
@@ -1047,10 +1245,11 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we see a large vertical gap, close open blocks
|
// If we see a large vertical gap, close open blocks
|
||||||
|
const lastProcessedLine = classifiedLines[i];
|
||||||
const currentNext = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null;
|
const currentNext = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null;
|
||||||
if (currentNext) {
|
if (currentNext && lastProcessedLine) {
|
||||||
const gap = currentNext.yCenter - current.yCenter;
|
const gap = currentNext.yCenter - lastProcessedLine.yCenter;
|
||||||
if (gap > avgHeight * 2.5) {
|
if (gap > avgHeight * 3.5) {
|
||||||
if (inChorus) { processedLines.push('{end_chorus}'); inChorus = false; }
|
if (inChorus) { processedLines.push('{end_chorus}'); inChorus = false; }
|
||||||
if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; }
|
if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; }
|
||||||
processedLines.push('');
|
processedLines.push('');
|
||||||
@@ -1120,7 +1319,7 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
if (!part.trim()) return part;
|
if (!part.trim()) return part;
|
||||||
let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
||||||
clean = this.sanitizeOcrChord(clean);
|
clean = this.sanitizeOcrChord(clean);
|
||||||
if (this.isChordWord(part, isItalian)) {
|
if (this.isChordWord(part, isItalian, true)) {
|
||||||
return `[${this.convertEnglishChordToItalian(clean, isItalian)}]`;
|
return `[${this.convertEnglishChordToItalian(clean, isItalian)}]`;
|
||||||
} else {
|
} else {
|
||||||
return part;
|
return part;
|
||||||
@@ -1234,7 +1433,7 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
|
|
||||||
let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
|
let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
|
||||||
cleanChord = this.sanitizeOcrChord(cleanChord);
|
cleanChord = this.sanitizeOcrChord(cleanChord);
|
||||||
if (this.isChordWord(chord.text, isItalian)) {
|
if (this.isChordWord(chord.text, isItalian, true)) {
|
||||||
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord, isItalian)}]`;
|
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord, isItalian)}]`;
|
||||||
} else {
|
} else {
|
||||||
wordResult += wordText.substring(lastCharIndex, charIndex);
|
wordResult += wordText.substring(lastCharIndex, charIndex);
|
||||||
|
|||||||
@@ -257,6 +257,52 @@ export class PlaylistService {
|
|||||||
this.syncLocalDataToServer();
|
this.syncLocalDataToServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clonePlaylist(name: string, ids: string[], songSettings?: any) {
|
||||||
|
await this.initPromise;
|
||||||
|
const isCommunityActive = this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive();
|
||||||
|
const key = 'playlists';
|
||||||
|
const lastKey = `lastPlaylist_${key}`;
|
||||||
|
|
||||||
|
// Create the cloned playlist
|
||||||
|
const newPlaylist = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name,
|
||||||
|
ids,
|
||||||
|
songSettings: songSettings || {},
|
||||||
|
createdAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
let personalPlaylists: any[] = [];
|
||||||
|
if (isCommunityActive) {
|
||||||
|
// If community is active, read the personal playlists from storage
|
||||||
|
const saved = await this._storage?.get(key);
|
||||||
|
personalPlaylists = saved || [];
|
||||||
|
} else {
|
||||||
|
// If personal context is active, we can use the current signal
|
||||||
|
personalPlaylists = this.playlists();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = [newPlaylist, ...personalPlaylists];
|
||||||
|
|
||||||
|
if (isCommunityActive) {
|
||||||
|
// Save directly to storage for personal context
|
||||||
|
await this._storage?.set(key, updated);
|
||||||
|
await this._storage?.set(`lastPlaylist_playlists`, newPlaylist);
|
||||||
|
} else {
|
||||||
|
// Update signal and save to storage
|
||||||
|
this.playlists.set(updated);
|
||||||
|
this.lastPlaylist.set(newPlaylist);
|
||||||
|
this.activeListIds.set(ids);
|
||||||
|
this.activeListName.set(name);
|
||||||
|
this.activePlaylistId.set(newPlaylist.id);
|
||||||
|
await this._storage?.set(key, updated);
|
||||||
|
await this._storage?.set(lastKey, newPlaylist);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
this.syncLocalDataToServer();
|
||||||
|
}
|
||||||
|
|
||||||
async replaceSongIdInPlaylists(oldId: string, newId: string) {
|
async replaceSongIdInPlaylists(oldId: string, newId: string) {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.playlists.update(p => p.map(pl => {
|
this.playlists.update(p => p.map(pl => {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const VERSION = '2026.06.18.0959';
|
export const VERSION = '2026.06.18.1346';
|
||||||
|
|||||||
Reference in New Issue
Block a user