feat: gestione trattini verticali | come accordi e allineamento grafico
This commit is contained in:
@@ -1,37 +1,45 @@
|
||||
<ion-header [translucent]="true" class="ion-no-border">
|
||||
<ion-toolbar class="bg-gradient top-toolbar">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button text="" defaultHref="/home" color="secondary"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title class="outfit-font wrapped-title">
|
||||
<div class="title-main" [style.fontSize.rem]="fontSize() * 1.1">
|
||||
<span class="canto-number" *ngIf="canto()?.id_canti">
|
||||
<ion-toolbar class="bg-gradient top-toolbar" style="--padding-top: 8px; --padding-bottom: 8px; --padding-start: 12px; --padding-end: 12px;">
|
||||
<div class="outfit-font" style="display: flex; flex-direction: column; width: 100%; gap: 2px;">
|
||||
|
||||
<!-- Row 1: Number + Title -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;">
|
||||
<span class="canto-number" *ngIf="canto()?.id_canti" style="flex-shrink: 0;">
|
||||
{{ canto()?.id?.startsWith('my_') ? getMySongNumber(canto()) : canto()?.id_canti }}
|
||||
</span>
|
||||
<span class="title-text" style="display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||
<span *ngIf="getCommunitySongNumber(canto())" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto()) }}</span>
|
||||
<span class="canto-title" style="font-weight: 700; font-size: 1.15rem; color: var(--ion-color-secondary); line-height: 1.2; white-space: normal; display: block; text-align: left; flex: 1; min-width: 0;">
|
||||
<span *ngIf="getCommunitySongNumber(canto())" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 6px; opacity: 0.9;">{{ getCommunitySongNumber(canto()) }}</span>
|
||||
<span>{{ canto()?.titolo || 'Player' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
||||
|
||||
<!-- Row 2: Author -->
|
||||
<div *ngIf="!isLandscapeActive()" style="display: block; width: 100%; padding-left: 40px; margin-top: 1px; margin-bottom: 2px;">
|
||||
<span class="song-author" style="font-size: 0.95rem; color: var(--ion-text-color); opacity: 0.8; font-weight: 400; text-align: left; display: block;">
|
||||
{{ canto()?.autore || 'Autore sconosciuto' }}
|
||||
</span>
|
||||
</div>
|
||||
<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'"
|
||||
[color]="showChords() ? 'secondary' : 'medium'"
|
||||
style="font-size: 1.3rem;">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
|
||||
<!-- Row 3: Badges only -->
|
||||
<div *ngIf="!isLandscapeActive()" class="scroll-horizontal" style="padding-left: 40px; margin-top: 2px; width: 100%;">
|
||||
<div style="display: flex; align-items: center; gap: inherit;">
|
||||
<ng-container *ngIf="settingsService.showDurationBpmTonality()">
|
||||
<span *ngIf="canto()?.durata" class="song-duration-badge" style="color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15); font-weight: normal;">
|
||||
<ion-icon name="time-outline"></ion-icon>
|
||||
{{ canto()?.durata }}
|
||||
</span>
|
||||
<span *ngIf="canto()?.bpm" class="song-bpm-badge" style="color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15); font-weight: normal;">
|
||||
<ion-icon name="pulse-outline"></ion-icon>
|
||||
{{ canto()?.bpm }} BPM
|
||||
</span>
|
||||
<span *ngIf="tonality()" class="song-tonality-badge" style="color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15); font-weight: normal;">
|
||||
<ion-icon name="musical-note-outline"></ion-icon>
|
||||
{{ tonality() }}
|
||||
</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-toolbar>
|
||||
|
||||
<!-- Audio Toolbar Removed -->
|
||||
@@ -145,6 +153,9 @@
|
||||
<ion-toolbar class="global-player-toolbar glass">
|
||||
<div class="player-content">
|
||||
<div class="controls-row">
|
||||
<ion-button fill="clear" color="secondary" (click)="goHome()" class="skip-btn">
|
||||
<ion-icon slot="icon-only" name="home-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="secondary" (click)="prevSong()" class="skip-btn">
|
||||
<ion-icon slot="icon-only" name="chevron-back"></ion-icon>
|
||||
</ion-button>
|
||||
@@ -209,6 +220,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons (edit, condividi, accordi) -->
|
||||
<div class="group">
|
||||
<ion-button fill="clear" size="small" (click)="editOrCloneCanto()" *ngIf="settingsService.showEditor()">
|
||||
<ion-icon slot="icon-only" name="create-outline" color="secondary"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" size="small" (click)="shareCanto()">
|
||||
<ion-icon slot="icon-only" name="share-social-outline" color="secondary"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" size="small" (click)="toggleChords()">
|
||||
<ion-icon slot="icon-only"
|
||||
[name]="showChords() ? 'musical-notes-outline' : 'text-outline'"
|
||||
[color]="showChords() ? 'secondary' : 'medium'">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="group">
|
||||
<ion-button fill="clear" size="small" (click)="restart()">
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
background: radial-gradient(circle at top left, #2d3436 0%, #121212 100%);
|
||||
}
|
||||
|
||||
.canto-title {
|
||||
transition: font-size 0.15s ease;
|
||||
&.small-title {
|
||||
font-size: 1.12rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.outfit-font {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
@@ -20,40 +27,56 @@
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.canto-number {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
background: rgba(var(--ion-color-secondary-rgb), 0.1);
|
||||
color: var(--ion-color-secondary);
|
||||
font-size: 0.85rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
border: 2px solid var(--ion-color-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
&:active { transform: scale(0.9); }
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--ion-color-secondary);
|
||||
color: #000;
|
||||
border-color: var(--ion-color-secondary);
|
||||
box-shadow: 0 0 10px rgba(var(--ion-color-secondary-rgb), 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.title-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
font-size: 1.15rem; // Fisso: non influenzato dallo zoom del testo
|
||||
font-weight: 700;
|
||||
color: var(--ion-color-secondary);
|
||||
white-space: normal;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
|
||||
.canto-number {
|
||||
font-size: 0.85em;
|
||||
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
||||
padding: 2px 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);
|
||||
flex-shrink: 0;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
&:active { transform: scale(0.9); }
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--ion-color-secondary);
|
||||
color: #000;
|
||||
border-color: var(--ion-color-secondary);
|
||||
box-shadow: 0 0 10px rgba(var(--ion-color-secondary-rgb), 0.4);
|
||||
}
|
||||
.song-author {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-weight: 400;
|
||||
display: inline-block;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
@@ -61,6 +84,102 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-horizontal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.song-duration-badge, .song-bpm-badge, .song-tonality-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
height: 26px;
|
||||
flex-shrink: 0;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
margin: 0;
|
||||
--padding-start: 4px;
|
||||
--padding-end: 4px;
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
flex-shrink: 0;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive scaling based on viewport width
|
||||
@media (max-width: 480px) {
|
||||
gap: 4px;
|
||||
|
||||
.song-duration-badge, .song-bpm-badge, .song-tonality-badge {
|
||||
font-size: 0.68rem;
|
||||
padding: 1px 3px;
|
||||
height: 22px;
|
||||
gap: 2px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
--padding-start: 0px;
|
||||
--padding-end: 0px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
gap: 2px;
|
||||
|
||||
.song-duration-badge, .song-bpm-badge, .song-tonality-badge {
|
||||
font-size: 0.62rem;
|
||||
padding: 1px 2px;
|
||||
height: 20px;
|
||||
gap: 1px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
--padding-start: 0px;
|
||||
--padding-end: 0px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content and Lyrics
|
||||
@@ -105,25 +224,14 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.play-btn {
|
||||
.play-btn, .skip-btn, .close-btn {
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.skip-btn, .close-btn {
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.4rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +252,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1.2rem;
|
||||
position: relative;
|
||||
|
||||
&.chorus {
|
||||
@@ -160,15 +268,15 @@
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--ion-color-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
opacity: 0.7;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.lyric-line {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.4rem;
|
||||
line-height: 1.35;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
min-height: 1.5em;
|
||||
@@ -731,6 +839,10 @@ ion-content.full-screen-content {
|
||||
}
|
||||
|
||||
:host-context(body.high-contrast) {
|
||||
.title-main .song-author {
|
||||
color: rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
.slim-toolbar {
|
||||
--background: #ffffff !important;
|
||||
background: #ffffff !important;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { FaceDetectorService } from '../../services/face-detector.service';
|
||||
export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
public canto = signal<Canto | null>(null);
|
||||
public activeSongId = signal<string | null>(null);
|
||||
public isTitleWrapped = signal<boolean>(false);
|
||||
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -132,6 +133,17 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
public transposeAmount = signal<number>(0);
|
||||
|
||||
public tonality = computed(() => {
|
||||
const song = this.canto();
|
||||
if (!song) return null;
|
||||
const baseKey = this.lyricsParser.deduceTonality(song.accordi || song.testo || '');
|
||||
if (!baseKey) return null;
|
||||
|
||||
const amount = this.transposeAmount();
|
||||
if (amount === 0) return baseKey;
|
||||
return this.lyricsParser.transposeChord(baseKey, amount);
|
||||
});
|
||||
|
||||
public currentLineIndex = signal<number>(0);
|
||||
public math = Math;
|
||||
public youtubePlayerService = inject(YoutubePlayerService);
|
||||
@@ -216,6 +228,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.playlistService.autoPlayPlaylist()) {
|
||||
setTimeout(() => this.initPlayer(c.id), 500);
|
||||
}
|
||||
this.isTitleWrapped.set(false);
|
||||
setTimeout(() => this.checkTitleWrap(), 150);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -366,6 +380,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
onResize(event: any) {
|
||||
this.windowLandscape.set(window.innerWidth > window.innerHeight);
|
||||
this.checkLandscapeZoom();
|
||||
this.checkTitleWrap();
|
||||
}
|
||||
|
||||
private isLandscape(): boolean {
|
||||
@@ -488,8 +503,23 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
checkTitleWrap() {
|
||||
const el = document.querySelector('.canto-title') as HTMLElement;
|
||||
if (!el) return;
|
||||
|
||||
this.isTitleWrapped.set(false);
|
||||
|
||||
setTimeout(() => {
|
||||
if (el.scrollHeight > 30) {
|
||||
this.isTitleWrapped.set(true);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.checkLandscapeZoom();
|
||||
this.checkTitleWrap();
|
||||
const gestureX = this.gestureCtrl.create({
|
||||
el: this.el.nativeElement,
|
||||
direction: 'x',
|
||||
@@ -686,6 +716,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.youtubePlayerService.seekTo(event.detail.value);
|
||||
}
|
||||
|
||||
goHome() {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.currentLineIndex.set(0);
|
||||
this.youtubePlayerService.seekTo(0);
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
|
||||
<ion-content class="bg-gradient">
|
||||
<div class="ion-padding">
|
||||
<div class="header-info glass ion-margin-bottom" *ngIf="localSongs.length > 0">
|
||||
<p>Trascina i canti per riordinarli. Una volta finito puoi salvare la playlist.</p>
|
||||
<div class="header-info glass ion-margin-bottom" *ngIf="localSongs.length > 0" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<p style="margin: 0;">Trascina i canti per riordinarli. Una volta finito puoi salvare la playlist.</p>
|
||||
<div *ngIf="totalDuration" style="background: rgba(var(--ion-color-secondary-rgb), 0.25); border: 1px solid var(--ion-color-secondary); padding: 4px 10px; border-radius: 10px; font-weight: 800; color: var(--ion-color-secondary); display: flex; align-items: center; gap: 4px; font-size: 0.85rem; font-family: 'Outfit', sans-serif; white-space: nowrap; margin-left: 12px;">
|
||||
<ion-icon name="time-outline" style="font-size: 1rem;"></ion-icon>
|
||||
{{ totalDuration }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div cdkDropList class="song-list" (cdkDropListDropped)="drop($event)">
|
||||
|
||||
@@ -51,6 +51,42 @@ export class PlaylistPage {
|
||||
public qrCodeImage: string | null = null;
|
||||
public savedPlaylistName: string | null = null;
|
||||
|
||||
get totalDuration(): string {
|
||||
const songs = this.localSongs;
|
||||
if (songs.length === 0) return '';
|
||||
let totalSeconds = 0;
|
||||
for (const song of songs) {
|
||||
if (song && song.durata) {
|
||||
totalSeconds += this.parseDuration(song.durata);
|
||||
}
|
||||
}
|
||||
if (totalSeconds === 0) return '';
|
||||
return this.formatTotalDuration(totalSeconds);
|
||||
}
|
||||
|
||||
private parseDuration(dur: string): number {
|
||||
if (!dur) return 0;
|
||||
const parts = dur.split(':').map(Number);
|
||||
if (parts.some(isNaN)) return 0;
|
||||
if (parts.length === 2) {
|
||||
return parts[0] * 60 + parts[1];
|
||||
} else if (parts.length === 3) {
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private formatTotalDuration(seconds: number): string {
|
||||
const hh = Math.floor(seconds / 3600);
|
||||
const mm = Math.floor((seconds % 3600) / 60);
|
||||
const ss = seconds % 60;
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
if (hh > 0) {
|
||||
return `${pad(hh)}:${pad(mm)}:${pad(ss)}`;
|
||||
}
|
||||
return `${pad(mm)}:${pad(ss)}`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Initial copy to allow local reordering
|
||||
this.localSongs = [...this.selectedSongs()];
|
||||
|
||||
@@ -50,6 +50,18 @@
|
||||
<ion-input [(ngModel)]="title" placeholder="Es: Il Signore è la mia salvezza"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<div class="category-selectors">
|
||||
<ion-item class="custom-input-item select-item">
|
||||
<ion-label position="stacked">Durata (mm:ss)</ion-label>
|
||||
<ion-input [(ngModel)]="durata" placeholder="Es: 03:45"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="custom-input-item select-item">
|
||||
<ion-label position="stacked">BPM (Tempo)</ion-label>
|
||||
<ion-input type="number" [(ngModel)]="bpm" placeholder="Es: 120"></ion-input>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="category-selectors">
|
||||
<ion-item class="custom-input-item select-item">
|
||||
<ion-label position="stacked">Momento Liturgico</ion-label>
|
||||
|
||||
@@ -192,6 +192,8 @@ export class ProposeCantoPage implements OnInit, OnDestroy {
|
||||
youtubeLink: string = '';
|
||||
selectedLiturgico: number[] = [];
|
||||
selectedTematico: number[] = [];
|
||||
durata: string = '';
|
||||
bpm: number | null = null;
|
||||
|
||||
get sortedLiturgico() {
|
||||
return [...this.cantiService.indiceLiturgico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
|
||||
@@ -296,6 +298,8 @@ export class ProposeCantoPage implements OnInit, OnDestroy {
|
||||
this.author = song.autore || '';
|
||||
this.youtubeLink = song.link_youtube || '';
|
||||
this.content = song.accordi || song.testo || '';
|
||||
this.durata = song.durata || '';
|
||||
this.bpm = song.bpm !== undefined ? song.bpm : null;
|
||||
|
||||
// Pre-populate liturgico and tematico lists
|
||||
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
|
||||
@@ -1702,7 +1706,9 @@ export class ProposeCantoPage implements OnInit, OnDestroy {
|
||||
link_youtube: this.youtubeLink,
|
||||
testo: this.content,
|
||||
accordi: this.content,
|
||||
id_momenti: id_momenti
|
||||
id_momenti: id_momenti,
|
||||
durata: this.durata || undefined,
|
||||
bpm: this.bpm !== null && this.bpm !== undefined && !isNaN(Number(this.bpm)) ? Number(this.bpm) : undefined
|
||||
});
|
||||
|
||||
// 2. Clone/convert remote playlist to local personal playlist
|
||||
@@ -1735,7 +1741,9 @@ export class ProposeCantoPage implements OnInit, OnDestroy {
|
||||
link_youtube: this.youtubeLink,
|
||||
testo: this.content,
|
||||
accordi: this.content, // Save to both fields for compatibility
|
||||
id_momenti: id_momenti
|
||||
id_momenti: id_momenti,
|
||||
durata: this.durata || undefined,
|
||||
bpm: this.bpm !== null && this.bpm !== undefined && !isNaN(Number(this.bpm)) ? Number(this.bpm) : undefined
|
||||
});
|
||||
|
||||
if (this.editId && !this.editId.startsWith('my_') && savedCanto && savedCanto.id) {
|
||||
|
||||
@@ -118,6 +118,14 @@
|
||||
</ion-label>
|
||||
<ion-toggle slot="end" [checked]="settingsService.showUpdateDate()" (ionChange)="settingsService.toggleShowUpdateDate()" color="secondary"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
<ion-icon name="musical-notes-outline" slot="start" color="secondary"></ion-icon>
|
||||
<ion-label class="outfit-font">
|
||||
<h2 class="settings-item-title">Visualizza durata, bpm e tonalità</h2>
|
||||
<p class="settings-item-subtitle">Mostra queste info sotto autore / titolo</p>
|
||||
</ion-label>
|
||||
<ion-toggle slot="end" [checked]="settingsService.showDurationBpmTonality()" (ionChange)="settingsService.toggleShowDurationBpmTonality()" color="secondary"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
<ion-icon name="swap-vertical-outline" slot="start" color="secondary"></ion-icon>
|
||||
<ion-label class="outfit-font">
|
||||
@@ -273,6 +281,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Importa Playlist -->
|
||||
<div class="settings-group glass ion-margin-bottom">
|
||||
<ion-item class="transparent-item" lines="none" button (click)="importPlaylistViaQr()">
|
||||
<ion-icon name="qr-code-outline" slot="start" color="secondary"></ion-icon>
|
||||
<ion-label class="outfit-font">
|
||||
<h2 class="settings-item-title">Importa Playlist via QR</h2>
|
||||
<p class="settings-item-subtitle">Inquadra il QR di una playlist condivisa per importarla</p>
|
||||
</ion-label>
|
||||
<ion-icon name="chevron-forward-outline" slot="end" color="medium" style="font-size: 1rem;"></ion-icon>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- Invio Dati Statistici -->
|
||||
<div class="settings-group glass ion-margin-bottom">
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
|
||||
@@ -77,6 +77,19 @@ export class SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
async importPlaylistViaQr() {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: QrScannerComponent
|
||||
});
|
||||
await modal.present();
|
||||
|
||||
const { data } = await modal.onWillDismiss();
|
||||
if (data) {
|
||||
// Naviga alla home passando il dato via state per il processing multi-scopo
|
||||
this.router.navigate(['/home'], { state: { scannedQrData: data } });
|
||||
}
|
||||
}
|
||||
|
||||
async manualRestore() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Ripristina con Codice',
|
||||
@@ -159,7 +172,9 @@ export class SettingsPage {
|
||||
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)) || []
|
||||
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || [],
|
||||
durata: item.durata || '',
|
||||
bpm: item.bpm !== undefined && item.bpm !== null ? Number(item.bpm) : undefined
|
||||
}));
|
||||
|
||||
if (customSongs.length > 0) {
|
||||
@@ -347,7 +362,9 @@ export class SettingsPage {
|
||||
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)) || []
|
||||
id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || [],
|
||||
durata: item.durata || '',
|
||||
bpm: item.bpm !== undefined && item.bpm !== null ? Number(item.bpm) : undefined
|
||||
}));
|
||||
|
||||
this.myCantiService.myCanti.set(customSongs);
|
||||
|
||||
Reference in New Issue
Block a user