Salvataggio modifiche correnti prima di implementare condivisione canto
This commit is contained in:
@@ -58,6 +58,27 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// Add global horizontal scroll support for wheel on horizontal containers
|
||||||
|
window.addEventListener('wheel', (event: WheelEvent) => {
|
||||||
|
if (Math.abs(event.deltaY) > 0 && Math.abs(event.deltaX) === 0) {
|
||||||
|
const path = event.composedPath();
|
||||||
|
for (const target of path) {
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
const style = window.getComputedStyle(target);
|
||||||
|
const isHorizontalScroll =
|
||||||
|
(style.overflowX === 'auto' || style.overflowX === 'scroll') &&
|
||||||
|
target.scrollWidth > target.clientWidth;
|
||||||
|
|
||||||
|
if (isHorizontalScroll) {
|
||||||
|
target.scrollLeft += event.deltaY;
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
// 1. Allineamento istantaneo alla versione remota
|
// 1. Allineamento istantaneo alla versione remota
|
||||||
if ((window as any).PwaLoader) {
|
if ((window as any).PwaLoader) {
|
||||||
(window as any).PwaLoader.update({
|
(window as any).PwaLoader.update({
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
<!-- Instructions when playlist is empty -->
|
<!-- Instructions when playlist is empty -->
|
||||||
<div class="empty-playlist-container" *ngIf="activeFilterType() === 'playlist' && playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()">
|
<div class="empty-playlist-container" *ngIf="activeFilterType() === 'playlist' && playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()">
|
||||||
<p class="empty-playlist-instruction outfit-font">
|
<p class="empty-playlist-instruction outfit-font">
|
||||||
Per creare una playlist, seleziona il nr dei canti che vuoi inserire nella playlist, riordinali e salvala con nome
|
Per creare una playlist, seleziona il nr del canto che vuoi inserire nella playlist, riordinali e salvala con nome
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|||||||
@@ -1318,7 +1318,7 @@ ion-title {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
text-align: center;
|
text-align: left;
|
||||||
background: rgba(var(--ion-color-secondary-rgb), 0.08);
|
background: rgba(var(--ion-color-secondary-rgb), 0.08);
|
||||||
border: 1px dashed rgba(var(--ion-color-secondary-rgb), 0.35);
|
border: 1px dashed rgba(var(--ion-color-secondary-rgb), 0.35);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
|||||||
@@ -1146,6 +1146,9 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
toggleSuggeriti() {
|
toggleSuggeriti() {
|
||||||
this.showSuggeriti.update(v => !v);
|
this.showSuggeriti.update(v => !v);
|
||||||
|
if (this.showSuggeriti()) {
|
||||||
|
this.isMassCardExpanded.set(true);
|
||||||
|
}
|
||||||
this.activeFilterType.set(null);
|
this.activeFilterType.set(null);
|
||||||
this.limit.set(30);
|
this.limit.set(30);
|
||||||
}
|
}
|
||||||
@@ -1481,7 +1484,10 @@ export class HomePage implements OnDestroy {
|
|||||||
handler: async (data) => {
|
handler: async (data) => {
|
||||||
if (data.name) {
|
if (data.name) {
|
||||||
const ids = this.reorderList().map(s => s.id);
|
const ids = this.reorderList().map(s => s.id);
|
||||||
await this.playlistService.savePlaylist(data.name, ids);
|
const activeId = this.playlistService.activePlaylistId();
|
||||||
|
const pl = activeId ? this.playlistService.playlists().find(p => p.id === activeId) : null;
|
||||||
|
const songSettings = pl ? pl.songSettings : undefined;
|
||||||
|
await this.playlistService.savePlaylist(data.name, ids, songSettings);
|
||||||
|
|
||||||
const toast = await this.toastCtrl.create({
|
const toast = await this.toastCtrl.create({
|
||||||
message: 'Playlist salvata!',
|
message: 'Playlist salvata!',
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
<ion-button fill="clear" (click)="zoomIn()" [disabled]="fontSize() >= maxZoom()">
|
<ion-button fill="clear" (click)="zoomIn()" [disabled]="fontSize() >= maxZoom()">
|
||||||
<ion-icon slot="icon-only" name="add-circle-outline" color="secondary"></ion-icon>
|
<ion-icon slot="icon-only" name="add-circle-outline" color="secondary"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<div class="side-indicator" style="display: flex; flex-direction: column; align-items: center; gap: 2px;">
|
||||||
|
<ion-icon name="search-outline" color="secondary" style="font-size: 1.0rem;"></ion-icon>
|
||||||
|
<span class="side-val" style="font-size: 0.7rem; font-weight: 700; color: var(--ion-color-secondary);">{{ fontSize().toFixed(1) }}</span>
|
||||||
|
</div>
|
||||||
<ion-button fill="clear" (click)="zoomOut()" [disabled]="fontSize() <= minZoom()">
|
<ion-button fill="clear" (click)="zoomOut()" [disabled]="fontSize() <= minZoom()">
|
||||||
<ion-icon slot="icon-only" name="remove-circle-outline" color="secondary"></ion-icon>
|
<ion-icon slot="icon-only" name="remove-circle-outline" color="secondary"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -229,6 +233,10 @@
|
|||||||
<ion-button fill="clear" size="small" (click)="zoomOut()" [disabled]="fontSize() <= minZoom()">
|
<ion-button fill="clear" size="small" (click)="zoomOut()" [disabled]="fontSize() <= minZoom()">
|
||||||
<ion-icon slot="icon-only" name="remove-circle-outline" color="secondary"></ion-icon>
|
<ion-icon slot="icon-only" name="remove-circle-outline" color="secondary"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<div class="zoom-indicator">
|
||||||
|
<ion-icon name="search-outline" color="secondary"></ion-icon>
|
||||||
|
<span class="val">{{ fontSize().toFixed(1) }}</span>
|
||||||
|
</div>
|
||||||
<ion-button fill="clear" size="small" (click)="zoomIn()" [disabled]="fontSize() >= maxZoom()">
|
<ion-button fill="clear" size="small" (click)="zoomIn()" [disabled]="fontSize() >= maxZoom()">
|
||||||
<ion-icon slot="icon-only" name="add-circle-outline" color="secondary"></ion-icon>
|
<ion-icon slot="icon-only" name="add-circle-outline" color="secondary"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -265,6 +265,17 @@
|
|||||||
ion-icon { font-size: 0.9rem; }
|
ion-icon { font-size: 0.9rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.zoom-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
|
||||||
|
ion-icon { font-size: 0.9rem; }
|
||||||
|
}
|
||||||
|
|
||||||
.camera-indicator {
|
.camera-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -306,7 +317,7 @@
|
|||||||
100% { transform: scale(0.9); opacity: 0.6; }
|
100% { transform: scale(0.9); opacity: 0.6; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
:host(.landscape-active) {
|
||||||
// Always hide footer in landscape as we have side controls
|
// Always hide footer in landscape as we have side controls
|
||||||
ion-footer {
|
ion-footer {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -330,8 +341,8 @@
|
|||||||
|
|
||||||
.landscape-side-controls {
|
.landscape-side-controls {
|
||||||
display: none !important; // Strict hidden in portrait
|
display: none !important; // Strict hidden in portrait
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
:host(.landscape-active) & {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -430,11 +441,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ion-content.full-screen-content {
|
ion-content.full-screen-content {
|
||||||
|
--offset-bottom: 48px !important; // Footer height
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.landscape-active) ion-content.full-screen-content {
|
||||||
--offset-bottom: 0px !important;
|
--offset-bottom: 0px !important;
|
||||||
|
|
||||||
@media (orientation: portrait) {
|
|
||||||
--offset-bottom: 48px !important; // Footer height
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mic-sensitivity-overlay {
|
.mic-sensitivity-overlay {
|
||||||
@@ -445,7 +456,7 @@ ion-content.full-screen-content {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
:host(.landscape-active) & {
|
||||||
right: 80px; // Prossimo ai controlli laterali
|
right: 80px; // Prossimo ai controlli laterali
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,10 +563,6 @@ ion-content.full-screen-content {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
|
||||||
left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-card {
|
.slider-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -846,7 +853,7 @@ ion-content.full-screen-content {
|
|||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
:host(.landscape-active) & {
|
||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
right: 80px;
|
right: 80px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import { FaceDetectorService } from '../../services/face-detector.service';
|
|||||||
selector: 'app-player',
|
selector: 'app-player',
|
||||||
templateUrl: './player.page.html',
|
templateUrl: './player.page.html',
|
||||||
styleUrls: ['./player.page.scss'],
|
styleUrls: ['./player.page.scss'],
|
||||||
|
host: {
|
||||||
|
'[class.landscape-active]': 'isLandscapeActive()'
|
||||||
|
},
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@@ -40,7 +43,11 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public isLandscapeActive = signal<boolean>(window.innerWidth > window.innerHeight);
|
private windowLandscape = signal<boolean>(window.innerWidth > window.innerHeight);
|
||||||
|
|
||||||
|
public isLandscapeActive = computed(() => {
|
||||||
|
return this.windowLandscape() && this.settingsService.landscapeProjectionEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
/** true = show chords (accordi mode), false = text only */
|
/** true = show chords (accordi mode), false = text only */
|
||||||
public showChords = signal<boolean>(false);
|
public showChords = signal<boolean>(false);
|
||||||
@@ -318,12 +325,12 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
onResize(event: any) {
|
onResize(event: any) {
|
||||||
this.isLandscapeActive.set(window.innerWidth > window.innerHeight);
|
this.windowLandscape.set(window.innerWidth > window.innerHeight);
|
||||||
this.checkLandscapeZoom();
|
this.checkLandscapeZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLandscape(): boolean {
|
private isLandscape(): boolean {
|
||||||
return window.innerWidth > window.innerHeight;
|
return this.isLandscapeActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkAndLimitFontSize(targetFont: number): number {
|
private checkAndLimitFontSize(targetFont: number): number {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class PlaylistPage {
|
|||||||
handler: (data) => {
|
handler: (data) => {
|
||||||
if (data.name) {
|
if (data.name) {
|
||||||
this.savedPlaylistName = data.name;
|
this.savedPlaylistName = data.name;
|
||||||
this.playlistService.savePlaylist(data.name, this.localSongs.map(s => s.id));
|
this.playlistService.savePlaylist(data.name, this.localSongs.map(s => s.id), this.getPlaylistSongSettings());
|
||||||
this.showToast('Playlist salvata!');
|
this.showToast('Playlist salvata!');
|
||||||
this.router.navigate(['/settings']);
|
this.router.navigate(['/settings']);
|
||||||
return true;
|
return true;
|
||||||
@@ -126,8 +126,8 @@ export class PlaylistPage {
|
|||||||
if (data.name) {
|
if (data.name) {
|
||||||
this.savedPlaylistName = data.name;
|
this.savedPlaylistName = data.name;
|
||||||
const ids = this.localSongs.map(s => s.id);
|
const ids = this.localSongs.map(s => s.id);
|
||||||
await this.playlistService.savePlaylist(data.name, ids);
|
|
||||||
const songSettings = this.getPlaylistSongSettings();
|
const songSettings = this.getPlaylistSongSettings();
|
||||||
|
await this.playlistService.savePlaylist(data.name, ids, songSettings);
|
||||||
this.qrCodeImage = await this.playlistService.generateQR(ids, data.name, songSettings);
|
this.qrCodeImage = await this.playlistService.generateQR(ids, data.name, songSettings);
|
||||||
this.showToast('Playlist salvata!');
|
this.showToast('Playlist salvata!');
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,17 +1,127 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { ProposeCantoPage } from './propose-canto.page';
|
import { ProposeCantoPage } from './propose-canto.page';
|
||||||
|
import { ToastController, PopoverController, NavController, AngularDelegate } from '@ionic/angular';
|
||||||
|
import { CantiService } from '../../services/canti.service';
|
||||||
|
import { MyCantiService } from '../../services/my-canti.service';
|
||||||
|
import { PlaylistService } from '../../services/playlist.service';
|
||||||
|
import { ThemeService } from '../../services/theme.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { LyricsParserService } from '../../services/lyrics-parser.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
describe('ProposeCantoPage', () => {
|
describe('ProposeCantoPage', () => {
|
||||||
let component: ProposeCantoPage;
|
let component: ProposeCantoPage;
|
||||||
let fixture: ComponentFixture<ProposeCantoPage>;
|
let fixture: ComponentFixture<ProposeCantoPage>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const cantiServiceMock = {
|
||||||
|
indiceLiturgico: () => [],
|
||||||
|
indiceTematico: () => [],
|
||||||
|
canti: () => []
|
||||||
|
};
|
||||||
|
const myCantiServiceMock = {
|
||||||
|
myCanti: () => []
|
||||||
|
};
|
||||||
|
const playlistServiceMock = {
|
||||||
|
remoteCustomSongs: () => [],
|
||||||
|
activePlaylistId: () => null
|
||||||
|
};
|
||||||
|
const themeServiceMock = {
|
||||||
|
highContrast: () => false
|
||||||
|
};
|
||||||
|
const activatedRouteMock = {
|
||||||
|
queryParams: of({})
|
||||||
|
};
|
||||||
|
const routerMock = {
|
||||||
|
navigate: jasmine.createSpy('navigate')
|
||||||
|
};
|
||||||
|
const lyricsParserMock = {
|
||||||
|
parseAccordi: () => []
|
||||||
|
};
|
||||||
|
const navCtrlMock = {};
|
||||||
|
const toastControllerMock = {};
|
||||||
|
const popoverControllerMock = {};
|
||||||
|
const angularDelegateMock = {};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: CantiService, useValue: cantiServiceMock },
|
||||||
|
{ provide: MyCantiService, useValue: myCantiServiceMock },
|
||||||
|
{ provide: PlaylistService, useValue: playlistServiceMock },
|
||||||
|
{ provide: ThemeService, useValue: themeServiceMock },
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||||
|
{ provide: Router, useValue: routerMock },
|
||||||
|
{ provide: LyricsParserService, useValue: lyricsParserMock },
|
||||||
|
{ provide: NavController, useValue: navCtrlMock },
|
||||||
|
{ provide: ToastController, useValue: toastControllerMock },
|
||||||
|
{ provide: PopoverController, useValue: popoverControllerMock },
|
||||||
|
{ provide: AngularDelegate, useValue: angularDelegateMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ProposeCantoPage);
|
fixture = TestBed.createComponent(ProposeCantoPage);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('convertEnglishChordToItalian', () => {
|
||||||
|
it('should convert simple English chords to Italian', () => {
|
||||||
|
expect(component.convertEnglishChordToItalian('C')).toBe('DO');
|
||||||
|
expect(component.convertEnglishChordToItalian('D')).toBe('RE');
|
||||||
|
expect(component.convertEnglishChordToItalian('E')).toBe('MI');
|
||||||
|
expect(component.convertEnglishChordToItalian('F')).toBe('FA');
|
||||||
|
expect(component.convertEnglishChordToItalian('G')).toBe('SOL');
|
||||||
|
expect(component.convertEnglishChordToItalian('A')).toBe('LA');
|
||||||
|
expect(component.convertEnglishChordToItalian('B')).toBe('SI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert English chords with accidentals and modifiers', () => {
|
||||||
|
expect(component.convertEnglishChordToItalian('C#m7')).toBe('DO#m7');
|
||||||
|
expect(component.convertEnglishChordToItalian('Bb')).toBe('SIb');
|
||||||
|
expect(component.convertEnglishChordToItalian('F#m')).toBe('FA#m');
|
||||||
|
expect(component.convertEnglishChordToItalian('Faug')).toBe('FAaug');
|
||||||
|
expect(component.convertEnglishChordToItalian('Fadd9')).toBe('FAadd9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep Italian chords unchanged', () => {
|
||||||
|
expect(component.convertEnglishChordToItalian('DO')).toBe('DO');
|
||||||
|
expect(component.convertEnglishChordToItalian('RE#m7')).toBe('RE#m7');
|
||||||
|
expect(component.convertEnglishChordToItalian('FA#')).toBe('FA#');
|
||||||
|
expect(component.convertEnglishChordToItalian('SIb')).toBe('SIb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle slash chords correctly', () => {
|
||||||
|
expect(component.convertEnglishChordToItalian('C/E')).toBe('DO/MI');
|
||||||
|
expect(component.convertEnglishChordToItalian('D/F#')).toBe('RE/FA#');
|
||||||
|
expect(component.convertEnglishChordToItalian('F#m7/A#')).toBe('FA#m7/LA#');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeOcrChord', () => {
|
||||||
|
it('should convert H to # when following a chord root', () => {
|
||||||
|
expect(component.sanitizeOcrChord('CH')).toBe('C#');
|
||||||
|
expect(component.sanitizeOcrChord('DHm7')).toBe('D#m7');
|
||||||
|
expect(component.sanitizeOcrChord('FH#')).toBe('F#');
|
||||||
|
expect(component.sanitizeOcrChord('FAH')).toBe('FA#');
|
||||||
|
expect(component.sanitizeOcrChord('SOLH7')).toBe('SOL#7');
|
||||||
|
expect(component.sanitizeOcrChord('FH#H')).toBe('F#');
|
||||||
|
expect(component.sanitizeOcrChord('FH#H/AH#')).toBe('F#/A#');
|
||||||
|
expect(component.sanitizeOcrChord('C#H-')).toBe('C#-');
|
||||||
|
expect(component.sanitizeOcrChord('G#H#')).toBe('G#');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert 0 to O for DO and SOL roots', () => {
|
||||||
|
expect(component.sanitizeOcrChord('D0')).toBe('DO');
|
||||||
|
expect(component.sanitizeOcrChord('D0#')).toBe('DO#');
|
||||||
|
expect(component.sanitizeOcrChord('S0L')).toBe('SOL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle slash chords with sanitization', () => {
|
||||||
|
expect(component.sanitizeOcrChord('CH/EH')).toBe('C#/E#');
|
||||||
|
expect(component.sanitizeOcrChord('D0/FH#')).toBe('DO/F#');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -380,12 +380,95 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
return this.parseSongSpatially(words);
|
return this.parseSongSpatially(words);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sanitizeOcrChord(text: string): string {
|
||||||
|
if (!text) return text;
|
||||||
|
if (text.includes('/')) {
|
||||||
|
return text.split('/').map(part => this.sanitizeOcrChord(part.trim())).join('/');
|
||||||
|
}
|
||||||
|
let cleaned = text;
|
||||||
|
// Replace D0/d0 with DO/do
|
||||||
|
cleaned = cleaned.replace(/^D0/gi, 'DO');
|
||||||
|
// Replace S0L/s0l with SOL/sol
|
||||||
|
cleaned = cleaned.replace(/^S0L/gi, 'SOL');
|
||||||
|
// Clean H/sharp mismatches:
|
||||||
|
cleaned = cleaned.replace(/H#/gi, '#');
|
||||||
|
cleaned = cleaned.replace(/#H/gi, '#');
|
||||||
|
cleaned = cleaned.replace(/([CDEFGAB]|DO|RE|MI|FA|SOL|LA|SI)H/gi, '$1#');
|
||||||
|
// Clean duplicate sharps (e.g. ## -> #)
|
||||||
|
cleaned = cleaned.replace(/##+/g, '#');
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertEnglishChordToItalian(chord: string): string {
|
||||||
|
if (!chord) return chord;
|
||||||
|
if (chord.includes('/')) {
|
||||||
|
return chord.split('/').map(part => this.convertEnglishChordToItalian(part.trim())).join('/');
|
||||||
|
}
|
||||||
|
const upper = chord.toUpperCase();
|
||||||
|
if (upper.startsWith('DO')) {
|
||||||
|
return chord;
|
||||||
|
}
|
||||||
|
if (upper.startsWith('FA')) {
|
||||||
|
if (upper.startsWith('FAUG') || upper.startsWith('FADD') || upper.startsWith('FALT')) {
|
||||||
|
return 'FA' + chord.slice(1);
|
||||||
|
}
|
||||||
|
return chord;
|
||||||
|
}
|
||||||
|
if (upper.startsWith('C')) {
|
||||||
|
return 'DO' + chord.slice(1);
|
||||||
|
}
|
||||||
|
if (upper.startsWith('D')) {
|
||||||
|
return 'RE' + chord.slice(1);
|
||||||
|
}
|
||||||
|
if (upper.startsWith('E')) {
|
||||||
|
return 'MI' + chord.slice(1);
|
||||||
|
}
|
||||||
|
if (upper.startsWith('F')) {
|
||||||
|
return 'FA' + chord.slice(1);
|
||||||
|
}
|
||||||
|
if (upper.startsWith('G')) {
|
||||||
|
return 'SOL' + chord.slice(1);
|
||||||
|
}
|
||||||
|
if (upper.startsWith('A')) {
|
||||||
|
return 'LA' + chord.slice(1);
|
||||||
|
}
|
||||||
|
if (upper.startsWith('B')) {
|
||||||
|
return 'SI' + chord.slice(1);
|
||||||
|
}
|
||||||
|
return chord;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLabelLine(text: string): boolean {
|
||||||
|
return /^(Intro|Strofa|Rit|Special|Coro|Bridge|RIT|CHORUS|VERSE)/i.test(text.trim());
|
||||||
|
}
|
||||||
|
|
||||||
parseSongSpatially(words: any[]): string {
|
parseSongSpatially(words: any[]): string {
|
||||||
if (!words || words.length === 0) {
|
if (!words || words.length === 0) {
|
||||||
console.warn('[OCR-Capture] Nessuna parola ricevuta dall\'OCR.');
|
console.warn('[OCR-Capture] Nessuna parola ricevuta dall\'OCR.');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preprocess words to split run-together chords like BA
|
||||||
|
const preprocessedWords: any[] = [];
|
||||||
|
words.forEach(w => {
|
||||||
|
if (!w.text) return;
|
||||||
|
const match = w.text.match(/^([ABCDEFG])([ABCDEFG])$/i);
|
||||||
|
if (match && !(match[1].toUpperCase() === 'F' && match[2].toUpperCase() === 'A')) {
|
||||||
|
const charWidth = (w.bbox.x1 - w.bbox.x0) / 2;
|
||||||
|
preprocessedWords.push({
|
||||||
|
text: match[1],
|
||||||
|
bbox: { ...w.bbox, x1: w.bbox.x0 + charWidth }
|
||||||
|
});
|
||||||
|
preprocessedWords.push({
|
||||||
|
text: match[2],
|
||||||
|
bbox: { ...w.bbox, x0: w.bbox.x0 + charWidth }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
preprocessedWords.push(w);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
words = preprocessedWords;
|
||||||
|
|
||||||
console.log(`[OCR-Capture] Parole totali ricevute dall'OCR: ${words.length}`);
|
console.log(`[OCR-Capture] Parole totali ricevute dall'OCR: ${words.length}`);
|
||||||
const validWords = words.filter(w => w.text && w.text.trim().length > 0);
|
const validWords = words.filter(w => w.text && w.text.trim().length > 0);
|
||||||
console.log(`[OCR-Capture] Parole valide dopo filtraggio: ${validWords.length}`);
|
console.log(`[OCR-Capture] Parole valide dopo filtraggio: ${validWords.length}`);
|
||||||
@@ -436,6 +519,12 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
let clean = text.toUpperCase().replace(/\s+/g, '');
|
let clean = text.toUpperCase().replace(/\s+/g, '');
|
||||||
clean = clean.replace(/\((.*?)\)/g, '/$1');
|
clean = clean.replace(/\((.*?)\)/g, '/$1');
|
||||||
clean = clean.replace(/[\.\,]$/g, '');
|
clean = clean.replace(/[\.\,]$/g, '');
|
||||||
|
clean = this.sanitizeOcrChord(clean);
|
||||||
|
if (clean.includes('_')) {
|
||||||
|
const parts = clean.split('_').filter(p => p.length > 0);
|
||||||
|
if (parts.length === 0) return false;
|
||||||
|
return parts.every(p => chordRegex.test(p));
|
||||||
|
}
|
||||||
return chordRegex.test(clean);
|
return chordRegex.test(clean);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -481,7 +570,8 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
|
|
||||||
if (current.isChords) {
|
if (current.isChords) {
|
||||||
const next = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null;
|
const next = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null;
|
||||||
if (next && !next.isChords) {
|
const nextText = next ? next.words.map(w => w.text).join(' ') : '';
|
||||||
|
if (next && !next.isChords && !this.isLabelLine(nextText)) {
|
||||||
// Merge spatially!
|
// Merge spatially!
|
||||||
const merged = this.mergeChordsAndLyrics(current.words, next.words);
|
const merged = this.mergeChordsAndLyrics(current.words, next.words);
|
||||||
const lineText = next.words.map(w => w.text).join(' ');
|
const lineText = next.words.map(w => w.text).join(' ');
|
||||||
@@ -502,21 +592,29 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
const multiChordRegex = /((?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?)?)/gi;
|
const multiChordRegex = /((?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?)?)/gi;
|
||||||
const expandedChords: string[] = [];
|
const expandedChords: string[] = [];
|
||||||
current.words.forEach(w => {
|
current.words.forEach(w => {
|
||||||
let cleanText = w.text.toUpperCase().replace(/\s+/g, '');
|
const parts = w.text.split(/(_)/);
|
||||||
cleanText = cleanText.replace(/\((.*?)\)/g, '/$1');
|
const processedParts = parts.map((part: string) => {
|
||||||
cleanText = cleanText.replace(/[\.\,]$/g, '');
|
if (part === '_') return ' _ ';
|
||||||
|
if (!part.trim()) return part;
|
||||||
if (chordRegex.test(cleanText)) {
|
|
||||||
expandedChords.push(`[${cleanText}]`);
|
let cleanText = part.toUpperCase().replace(/\s+/g, '');
|
||||||
} else {
|
cleanText = cleanText.replace(/\((.*?)\)/g, '/$1');
|
||||||
const matches = [...cleanText.matchAll(multiChordRegex)];
|
cleanText = cleanText.replace(/[\.\,]$/g, '');
|
||||||
const fullMatchStr = matches.map(m => m[0]).join('');
|
cleanText = this.sanitizeOcrChord(cleanText);
|
||||||
if (matches.length > 0 && fullMatchStr === cleanText) {
|
|
||||||
expandedChords.push(...matches.map((m: string) => `[${m[0].toUpperCase()}]`));
|
if (chordRegex.test(cleanText)) {
|
||||||
|
return `[${this.convertEnglishChordToItalian(cleanText)}]`;
|
||||||
} else {
|
} else {
|
||||||
expandedChords.push(w.text); // keep original text if it's not a pure chord merge
|
const matches = [...cleanText.matchAll(multiChordRegex)];
|
||||||
|
const fullMatchStr = matches.map(m => m[0]).join('');
|
||||||
|
if (matches.length > 0 && fullMatchStr === cleanText) {
|
||||||
|
return matches.map((m: any) => `[${this.convertEnglishChordToItalian(m[0].toUpperCase())}]`).join(' ');
|
||||||
|
} else {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
expandedChords.push(processedParts.join(''));
|
||||||
});
|
});
|
||||||
const wrapped = expandedChords.join(' ');
|
const wrapped = expandedChords.join(' ');
|
||||||
if (wrapped) {
|
if (wrapped) {
|
||||||
@@ -535,7 +633,7 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
inVerse = true;
|
inVerse = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
processedLines.push(lineText);
|
processedLines.push(this.wrapChords(lineText, chordRegex));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we see a large vertical gap, close open blocks
|
// If we see a large vertical gap, close open blocks
|
||||||
@@ -606,8 +704,14 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
|
|
||||||
if (!textWords || textWords.length === 0) {
|
if (!textWords || textWords.length === 0) {
|
||||||
return mergedChordWords.map(c => {
|
return mergedChordWords.map(c => {
|
||||||
let clean = c.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
const parts = c.text.split(/(_)/);
|
||||||
return `[${clean}]`;
|
return parts.map((part: string) => {
|
||||||
|
if (part === '_') return ' _ ';
|
||||||
|
if (!part.trim()) return part;
|
||||||
|
let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
||||||
|
clean = this.sanitizeOcrChord(clean);
|
||||||
|
return `[${this.convertEnglishChordToItalian(clean)}]`;
|
||||||
|
}).join('');
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,30 +720,54 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
|
|
||||||
const expandedChordWords: any[] = [];
|
const expandedChordWords: any[] = [];
|
||||||
mergedChordWords.forEach(chord => {
|
mergedChordWords.forEach(chord => {
|
||||||
let originalText = chord.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
const parts = chord.text.split(/(_)/);
|
||||||
const matches = [...originalText.matchAll(multiChordRegex)];
|
let currentX = chord.bbox.x0;
|
||||||
|
const totalLen = chord.text.length || 1;
|
||||||
|
const widthPerChar = (chord.bbox.x1 - chord.bbox.x0) / totalLen;
|
||||||
|
|
||||||
if (matches.length === 0) {
|
parts.forEach((part: string) => {
|
||||||
expandedChordWords.push(chord);
|
const partLen = part.length;
|
||||||
} else {
|
const partWidth = partLen * widthPerChar;
|
||||||
const fullMatchStr = matches.map(m => m[0]).join('');
|
const partX0 = currentX;
|
||||||
if (fullMatchStr === originalText) {
|
const partX1 = currentX + partWidth;
|
||||||
const charWidth = (chord.bbox.x1 - chord.bbox.x0) / Math.max(1, originalText.length);
|
currentX = partX1;
|
||||||
matches.forEach(match => {
|
|
||||||
const matchIndex = match.index!;
|
if (part === '_' || !part.trim()) {
|
||||||
const matchLength = match[0].length;
|
return;
|
||||||
const newX0 = chord.bbox.x0 + matchIndex * charWidth;
|
}
|
||||||
const newX1 = chord.bbox.x0 + (matchIndex + matchLength) * charWidth;
|
|
||||||
|
let originalText = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
|
||||||
expandedChordWords.push({
|
originalText = this.sanitizeOcrChord(originalText);
|
||||||
text: match[0],
|
const matches = [...originalText.matchAll(multiChordRegex)];
|
||||||
bbox: { ...chord.bbox, x0: newX0, x1: newX1 }
|
|
||||||
});
|
if (matches.length === 0) {
|
||||||
|
expandedChordWords.push({
|
||||||
|
text: originalText,
|
||||||
|
bbox: { ...chord.bbox, x0: partX0, x1: partX1 }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
expandedChordWords.push(chord);
|
const fullMatchStr = matches.map(m => m[0]).join('');
|
||||||
|
if (fullMatchStr === originalText) {
|
||||||
|
const matchCharWidth = (partX1 - partX0) / Math.max(1, originalText.length);
|
||||||
|
matches.forEach(match => {
|
||||||
|
const matchIndex = match.index!;
|
||||||
|
const matchLength = match[0].length;
|
||||||
|
const newX0 = partX0 + matchIndex * matchCharWidth;
|
||||||
|
const newX1 = partX0 + (matchIndex + matchLength) * matchCharWidth;
|
||||||
|
|
||||||
|
expandedChordWords.push({
|
||||||
|
text: match[0],
|
||||||
|
bbox: { ...chord.bbox, x0: newX0, x1: newX1 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
expandedChordWords.push({
|
||||||
|
text: originalText,
|
||||||
|
bbox: { ...chord.bbox, x0: partX0, x1: partX1 }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const chordAssignments = new Map<any, any[]>();
|
const chordAssignments = new Map<any, any[]>();
|
||||||
@@ -691,8 +819,9 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
if (charIndex < 0) charIndex = 0;
|
if (charIndex < 0) charIndex = 0;
|
||||||
if (charIndex > wordText.length) charIndex = wordText.length;
|
if (charIndex > wordText.length) charIndex = wordText.length;
|
||||||
|
|
||||||
const cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
|
let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
|
||||||
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${cleanChord}]`;
|
cleanChord = this.sanitizeOcrChord(cleanChord);
|
||||||
|
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord)}]`;
|
||||||
lastCharIndex = charIndex;
|
lastCharIndex = charIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -704,10 +833,15 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalResult = result.replace(/\]\[/g, '] [');
|
let finalResult = result.replace(/\]\[/g, '] [');
|
||||||
|
finalResult = finalResult.replace(/\]_\[/g, '] _ [');
|
||||||
|
finalResult = finalResult.replace(/\]_/g, '] _ ');
|
||||||
|
finalResult = finalResult.replace(/_\[/g, ' _ [');
|
||||||
|
// Normalize any duplicate spaces around underscores:
|
||||||
|
finalResult = finalResult.replace(/\s*_\s*/g, ' _ ');
|
||||||
|
|
||||||
console.log(`[OCR-Debug] Linea generata: ${finalResult}`);
|
console.log(`[OCR-Debug] Linea generata: ${finalResult}`);
|
||||||
|
|
||||||
// Assicura che due accordi consecutivi abbiano sempre 3 spazi (es. [LA][MI] diventa [LA] [MI])
|
|
||||||
return finalResult;
|
return finalResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,11 +884,22 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private wrapChords(line: string, regex: RegExp): string {
|
private wrapChords(line: string, regex: RegExp): string {
|
||||||
const chordsInLine = line.match(regex);
|
const globalRegex = new RegExp(regex.source.replace(/^\^/, '').replace(/\$$/, ''), 'gi');
|
||||||
if (chordsInLine && chordsInLine.length > 0) {
|
const parts = line.split(/(_)/);
|
||||||
return line.replace(regex, (match) => `[${match.toUpperCase()}]`);
|
return parts.map((part: string) => {
|
||||||
}
|
if (part === '_') return ' _ ';
|
||||||
return line;
|
return part.replace(globalRegex, (match) => {
|
||||||
|
const lower = match.toLowerCase();
|
||||||
|
if (match === 'a' || match === 'e' || match === 'o' || match === 'i') {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (['la', 'mi', 're', 'do', 'si', 'fa', 'sol'].includes(lower) && match === lower) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
const sanitized = this.sanitizeOcrChord(match.toUpperCase());
|
||||||
|
return `[${this.convertEnglishChordToItalian(sanitized)}]`;
|
||||||
|
});
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,14 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-toggle slot="end" [checked]="settingsService.karaokePageScrollMode()" (ionChange)="settingsService.toggleKaraokePageScrollMode()" color="secondary"></ion-toggle>
|
<ion-toggle slot="end" [checked]="settingsService.karaokePageScrollMode()" (ionChange)="settingsService.toggleKaraokePageScrollMode()" color="secondary"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<ion-item class="transparent-item" lines="none" style="border-top: 1px solid rgba(255,255,255,0.03);">
|
||||||
|
<ion-icon name="phone-landscape-outline" slot="start" color="secondary"></ion-icon>
|
||||||
|
<ion-label class="outfit-font">
|
||||||
|
<h2 class="settings-item-title">Vista orizzontale per proiezione</h2>
|
||||||
|
<p class="settings-item-subtitle">Adatta il layout in landscape per la proiezione (testo più grande, controlli dedicati)</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-toggle slot="end" [checked]="settingsService.landscapeProjectionEnabled()" (ionChange)="settingsService.toggleLandscapeProjectionEnabled()" color="secondary"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comunità -->
|
<!-- Comunità -->
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export class SettingsService {
|
|||||||
/** Avanzamento a pagine del karaoke manuale: true = i tasti next/prev voltano la pagina */
|
/** Avanzamento a pagine del karaoke manuale: true = i tasti next/prev voltano la pagina */
|
||||||
public karaokePageScrollMode = signal<boolean>(false);
|
public karaokePageScrollMode = signal<boolean>(false);
|
||||||
|
|
||||||
|
/** Vista orizzontale per proiezione: true = attiva layout landscape per proiezione */
|
||||||
|
public landscapeProjectionEnabled = signal<boolean>(true);
|
||||||
|
|
||||||
/** Identificativo utente univoco per la gestione delle comunità */
|
/** Identificativo utente univoco per la gestione delle comunità */
|
||||||
public userUuid = signal<string>('');
|
public userUuid = signal<string>('');
|
||||||
|
|
||||||
@@ -244,6 +247,13 @@ export class SettingsService {
|
|||||||
this.karaokePageScrollMode.set(false);
|
this.karaokePageScrollMode.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedLandscapeProjectionEnabled = localStorage.getItem('landscape-projection-enabled');
|
||||||
|
if (savedLandscapeProjectionEnabled !== null) {
|
||||||
|
this.landscapeProjectionEnabled.set(savedLandscapeProjectionEnabled === 'true');
|
||||||
|
} else {
|
||||||
|
this.landscapeProjectionEnabled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync browser fullscreen state with listeners (supporting vendor prefixes)
|
// Sync browser fullscreen state with listeners (supporting vendor prefixes)
|
||||||
const updateFullscreenState = () => {
|
const updateFullscreenState = () => {
|
||||||
const isFs = !!(
|
const isFs = !!(
|
||||||
@@ -297,6 +307,10 @@ export class SettingsService {
|
|||||||
localStorage.setItem('karaoke-page-scroll-mode', this.karaokePageScrollMode().toString());
|
localStorage.setItem('karaoke-page-scroll-mode', this.karaokePageScrollMode().toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
localStorage.setItem('landscape-projection-enabled', this.landscapeProjectionEnabled().toString());
|
||||||
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const active = this.keepScreenOn();
|
const active = this.keepScreenOn();
|
||||||
localStorage.setItem('keep-screen-on', active.toString());
|
localStorage.setItem('keep-screen-on', active.toString());
|
||||||
@@ -426,6 +440,12 @@ export class SettingsService {
|
|||||||
localStorage.setItem('karaoke-page-scroll-mode', newValue.toString());
|
localStorage.setItem('karaoke-page-scroll-mode', newValue.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleLandscapeProjectionEnabled() {
|
||||||
|
const newValue = !this.landscapeProjectionEnabled();
|
||||||
|
this.landscapeProjectionEnabled.set(newValue);
|
||||||
|
localStorage.setItem('landscape-projection-enabled', newValue.toString());
|
||||||
|
}
|
||||||
|
|
||||||
setChordNotationPreference(val: 'diesis' | 'bemolle') {
|
setChordNotationPreference(val: 'diesis' | 'bemolle') {
|
||||||
this.chordNotationPreference.set(val);
|
this.chordNotationPreference.set(val);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const VERSION = '2026.06.15.0021';
|
export const VERSION = '2026.06.16.1611';
|
||||||
|
|||||||
Reference in New Issue
Block a user