modifiche face offline e 2 secondi di attesa
This commit is contained in:
@@ -172,6 +172,13 @@ export class AppComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
console.log('[AppComponent] Running on localhost - skipping protocol link redirect on appinstalled.');
|
||||
this.isRedirecting.set(false);
|
||||
this.checkLoaderDismissal();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = this.protocolLink;
|
||||
}, 1000);
|
||||
@@ -377,6 +384,12 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
|
||||
async checkAndRedirectToPwa() {
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
console.log('[AppComponent] Running on localhost - skipping PWA redirect and install overlays.');
|
||||
this.checkLoaderDismissal();
|
||||
return;
|
||||
}
|
||||
|
||||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
|
||||
if (isStandalone) {
|
||||
localStorage.setItem('pwa-installed', 'true');
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
(click)="toggleFilterType('playlist')"
|
||||
class="filter-chip">
|
||||
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
|
||||
<span *ngIf="isRemotePlaylist() && playlistService.hasRemotePlaylistUpdate()" (click)="refreshActiveRemotePlaylist(); $event.stopPropagation();" style="display: inline-flex; align-items: center; justify-content: center; padding: 2px; margin-left: 6px; background: rgba(var(--ion-color-secondary-rgb), 0.25); border-radius: 50%; border: 1px solid var(--ion-color-secondary); width: 20px; height: 20px; box-shadow: 0 0 8px rgba(var(--ion-color-secondary-rgb), 0.3); vertical-align: middle;">
|
||||
<ion-icon name="refresh-outline" style="font-size: 0.95rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
|
||||
</span>
|
||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null"></ion-icon>
|
||||
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
|
||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||
@@ -159,6 +162,9 @@
|
||||
|
||||
<!-- Active Playlist Actions -->
|
||||
<div class="selection-pill glass" *ngIf="activeFilterType() === 'playlist' && playlistService.activePlaylistId() && !playlistService.selectionMode()">
|
||||
<ion-button fill="clear" color="secondary" (click)="refreshActiveRemotePlaylist()" class="mini-action-btn" title="Aggiorna Playlist" *ngIf="isRemotePlaylist()">
|
||||
<ion-icon slot="icon-only" name="refresh-outline" [style.color]="playlistService.hasRemotePlaylistUpdate() ? 'var(--ion-color-warning)' : 'inherit'"></ion-icon>
|
||||
</ion-button>
|
||||
<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-button>
|
||||
|
||||
@@ -476,6 +476,9 @@ export class HomePage implements OnDestroy {
|
||||
// Polling setup: check for updates every 30 seconds
|
||||
this.updatePollInterval = setInterval(() => {
|
||||
this.checkUpdateStatus();
|
||||
if (this.isRemotePlaylist()) {
|
||||
this.playlistService.checkForRemotePlaylistUpdates();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
|
||||
@@ -1251,6 +1254,9 @@ export class HomePage implements OnDestroy {
|
||||
this.playlistService.activeListIds.set(pl.ids);
|
||||
this.playlistService.activeListName.set(pl.name);
|
||||
this.playlistService.activePlaylistId.set(pl.id);
|
||||
if (pl.isRemote || pl.id?.startsWith('remote_')) {
|
||||
this.playlistService.checkForRemotePlaylistUpdates();
|
||||
}
|
||||
// Mantieni il menu aperto poiché la playlist è ora selezionata ed attiva
|
||||
this.limit.set(50);
|
||||
}
|
||||
@@ -1738,6 +1744,31 @@ export class HomePage implements OnDestroy {
|
||||
return !!id && id.startsWith('remote_');
|
||||
}
|
||||
|
||||
async refreshActiveRemotePlaylist() {
|
||||
const loading = await this.loadingCtrl.create({
|
||||
message: 'Aggiornamento playlist...'
|
||||
});
|
||||
await loading.present();
|
||||
try {
|
||||
await this.playlistService.refreshRemotePlaylist();
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Playlist aggiornata da remoto!',
|
||||
duration: 2000,
|
||||
color: 'success'
|
||||
});
|
||||
await toast.present();
|
||||
} catch (e) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Errore',
|
||||
message: 'Impossibile aggiornare la playlist da remoto.',
|
||||
buttons: ['OK']
|
||||
});
|
||||
await alert.present();
|
||||
} finally {
|
||||
await loading.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
isActivePlaylistSaved(): boolean {
|
||||
const id = this.playlistService.activePlaylistId();
|
||||
if (!id) return false;
|
||||
@@ -1807,7 +1838,9 @@ export class HomePage implements OnDestroy {
|
||||
color: 'success'
|
||||
});
|
||||
await toast.present();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -109,8 +109,24 @@
|
||||
<!-- EDITOR AREA -->
|
||||
<div class="editor-wrapper" [class.hc]="isHighContrast">
|
||||
<div class="editor-header">
|
||||
<div class="editor-title-group">
|
||||
<div class="editor-title-group" style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>Editor Testo</span>
|
||||
|
||||
<!-- Transpose buttons in left column, above textarea -->
|
||||
<div class="editor-transpose-group" style="display: flex; align-items: center; gap: 4px; background: rgba(255, 255, 255, 0.05); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); margin-left: 12px;">
|
||||
<ion-button fill="clear" size="small" (click)="transposeDown()" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 24px;">
|
||||
<ion-icon slot="icon-only" name="remove-outline" style="font-size: 1.0rem; color: var(--ion-color-secondary);"></ion-icon>
|
||||
</ion-button>
|
||||
<span class="val outfit-font" style="font-size: 0.85rem; font-weight: 700; color: var(--ion-color-secondary); min-width: 24px; text-align: center;">T:{{ transposeAmount > 0 ? '+' : '' }}{{ transposeAmount }}</span>
|
||||
<ion-button fill="clear" size="small" (click)="transposeUp()" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 24px;">
|
||||
<ion-icon slot="icon-only" name="add-outline" style="font-size: 1.0rem; color: var(--ion-color-secondary);"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<!-- Set default key button -->
|
||||
<ion-button *ngIf="transposeAmount !== 0" fill="outline" color="secondary" size="small" (click)="setDefaultTonalita()" style="margin: 0 0 0 8px; font-size: 10px; font-weight: 700; height: 24px; --border-radius: 6px;">
|
||||
Imposta tonalità di default
|
||||
</ion-button>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0" title="Annulla ultima modifica">
|
||||
@@ -127,25 +143,55 @@
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
<ion-item class="custom-input-item textarea-item">
|
||||
<ion-textarea
|
||||
#contentTextarea
|
||||
<div class="editor-textarea-container">
|
||||
<!-- Input textarea -->
|
||||
<textarea
|
||||
#nativeTextarea
|
||||
[(ngModel)]="content"
|
||||
placeholder="Scrivi o scansiona..."
|
||||
rows="18"
|
||||
class="content-textarea"
|
||||
(paste)="onPaste($event)">
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
(paste)="onPaste($event)"
|
||||
spellcheck="false"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-item class="custom-input-item">
|
||||
<ion-label position="stacked">Autore / Link YouTube</ion-label>
|
||||
<ion-label position="stacked">Autore</ion-label>
|
||||
<ion-input [(ngModel)]="author" placeholder="Autore"></ion-input>
|
||||
<ion-input [(ngModel)]="youtubeLink" placeholder="URL YouTube"></ion-input>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<!-- YouTube Link and Player Section -->
|
||||
<div class="custom-input-item youtube-section">
|
||||
<ion-label position="stacked" style="color: #64ffda; font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">Link YouTube & Audio Player</ion-label>
|
||||
<div class="youtube-input-row" style="display: flex; align-items: center; gap: 8px; margin-top: 6px;">
|
||||
<ion-input [(ngModel)]="youtubeLink" placeholder="URL YouTube" style="flex: 1;"></ion-input>
|
||||
<ion-button fill="solid" color="secondary" size="small" [disabled]="!youtubeLink" (click)="loadEditorAudio()" style="margin: 0; font-weight: 700;">
|
||||
Carica
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<!-- Small Player controls when playing/loaded -->
|
||||
<div class="editor-mini-player" *ngIf="isAudioLoaded()" style="display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 8px; background: rgba(255, 255, 255, 0.05); border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1);">
|
||||
<ion-button fill="clear" color="secondary" (click)="togglePlayPause()" class="play-btn" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 36px; width: 36px;">
|
||||
<ion-icon slot="icon-only" [name]="youtubePlayerService.isPlaying() ? 'pause-sharp' : 'play-sharp'" style="font-size: 1.6rem;"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<span class="player-time outfit-font" style="font-size: 0.8rem; color: var(--ion-color-secondary); opacity: 0.8; min-width: 32px; text-align: right;">{{ formatSeconds(youtubePlayerService.videoProgress()) }}</span>
|
||||
<ion-range
|
||||
[min]="0"
|
||||
[max]="youtubePlayerService.videoDuration()"
|
||||
[value]="youtubePlayerService.videoProgress()"
|
||||
(ionChange)="onSeek($event)"
|
||||
color="secondary"
|
||||
style="margin: 0; padding: 0 4px; flex: 1; --bar-height: 4px; --knob-size: 12px;">
|
||||
</ion-range>
|
||||
<span class="player-time outfit-font" style="font-size: 0.8rem; color: var(--ion-color-secondary); opacity: 0.8; min-width: 32px; text-align: left;">{{ formatSeconds(youtubePlayerService.videoDuration()) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ion-list>
|
||||
|
||||
<div class="action-buttons">
|
||||
<ion-button expand="block" (click)="saveToMyCanti()" class="send-btn" [disabled]="!title || !content || isProcessingOCR">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
@@ -153,13 +199,14 @@
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Right Column: Active Preview Pane -->
|
||||
<div class="preview-column" [class.hide-on-mobile]="activeTab === 'editor'">
|
||||
<div class="preview-card" [class.hc]="isHighContrast">
|
||||
<div class="preview-header">
|
||||
<span class="preview-title">Anteprima Attiva</span>
|
||||
<ion-button fill="clear" size="small" (click)="toggleChordsPreview()" class="preview-toggle-btn">
|
||||
|
||||
<ion-button fill="clear" size="small" (click)="toggleChordsPreview()" class="preview-toggle-btn" style="margin: 0;">
|
||||
<ion-icon slot="start" [name]="showChordsPreview ? 'musical-notes-outline' : 'text-outline'"></ion-icon>
|
||||
{{ showChordsPreview ? 'Con Accordi' : 'Solo Testo' }}
|
||||
</ion-button>
|
||||
@@ -170,26 +217,28 @@
|
||||
<p class="preview-song-author" *ngIf="author">{{ author }}</p>
|
||||
</div>
|
||||
<div class="preview-lyrics-container">
|
||||
<div *ngFor="let section of parsedSections"
|
||||
class="preview-section"
|
||||
[class.chorus]="section.type === 'chorus'"
|
||||
[class.verse-num]="section.type === 'verse_num'">
|
||||
|
||||
<div *ngIf="section.type === 'chorus'" class="preview-section-label">Rit.</div>
|
||||
<div *ngIf="section.type === 'verse_num' && section.verseNumber" class="preview-section-label preview-verse-num-label">{{ section.verseNumber }}.</div>
|
||||
|
||||
<div *ngFor="let line of section.lines" class="preview-lyric-line">
|
||||
<!-- Chord mode -->
|
||||
<ng-container *ngIf="showChordsPreview; else textOnly">
|
||||
<span *ngFor="let seg of line.segments; let i = index" class="preview-chord-segment" [class.contiguous-next]="lyricsParser.isContiguousNext(line.segments, i)">
|
||||
<span *ngIf="seg.chord" class="preview-chord">{{ seg.chord }}</span>
|
||||
<span class="preview-seg-text">{{ seg.text }}</span>
|
||||
</span>
|
||||
</ng-container>
|
||||
<!-- Text only mode -->
|
||||
<ng-template #textOnly>
|
||||
{{ line.text }}
|
||||
</ng-template>
|
||||
<div class="lyrics-view">
|
||||
<div *ngFor="let section of parsedSections"
|
||||
class="section"
|
||||
[class.chorus]="section.type === 'chorus'"
|
||||
[class.verse-num]="section.type === 'verse_num'">
|
||||
|
||||
<div *ngIf="section.type === 'chorus'" class="section-label">Rit.</div>
|
||||
<div *ngIf="section.type === 'verse_num' && section.verseNumber" class="section-label verse-num-label">{{ section.verseNumber }}.</div>
|
||||
|
||||
<div *ngFor="let line of section.lines" class="lyric-line">
|
||||
<!-- Chord mode -->
|
||||
<ng-container *ngIf="showChordsPreview; else textOnly">
|
||||
<span *ngFor="let seg of line.segments; let i = index" class="chord-segment" [class.contiguous-next]="lyricsParser.isContiguousNext(line.segments, i)">
|
||||
<span *ngIf="seg.chord" class="chord">{{ seg.chord }}</span>
|
||||
<span class="seg-text">{{ seg.text }}</span>
|
||||
</span>
|
||||
</ng-container>
|
||||
<!-- Text only mode -->
|
||||
<ng-template #textOnly>
|
||||
{{ line.text }}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,8 +252,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Hidden inputs -->
|
||||
<input type="file" #cameraInput (change)="onFileSelected($event, true)" accept="image/*" capture="camera" style="display: none;">
|
||||
<input type="file" #fileInput (change)="onFileSelected($event, false)" accept="image/*,application/pdf" style="display: none;">
|
||||
|
||||
@@ -94,11 +94,14 @@ body.high-contrast :host ::ng-deep {
|
||||
}
|
||||
|
||||
/* Textarea inside high contrast must have white background and black text */
|
||||
.content-textarea {
|
||||
--color: #000000 !important;
|
||||
color: #000000 !important;
|
||||
.editor-textarea-container {
|
||||
background: #ffffff !important;
|
||||
--background: #ffffff !important;
|
||||
border: 2px solid #000000 !important;
|
||||
|
||||
textarea {
|
||||
color: #000000 !important;
|
||||
caret-color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Force background of inputs and items to be white with single dark gray border in high contrast */
|
||||
@@ -267,30 +270,49 @@ body.high-contrast :host ::ng-deep {
|
||||
}
|
||||
}
|
||||
|
||||
.content-textarea {
|
||||
--color: #000000 !important;
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
--background: #ffffff !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: 700 !important;
|
||||
caret-color: #000000;
|
||||
.editor-textarea-container {
|
||||
background: #ffffff;
|
||||
|
||||
textarea {
|
||||
color: #000000;
|
||||
caret-color: #000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
.content-textarea {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
--color: #ffffff !important;
|
||||
color: #ffffff !important;
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--padding-top: 16px;
|
||||
--padding-bottom: 16px;
|
||||
min-height: 400px;
|
||||
.editor-textarea-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
background: #111111;
|
||||
border-radius: 0 0 14px 14px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0 !important;
|
||||
padding: 16px !important;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
|
||||
font-size: 1.05rem !important;
|
||||
line-height: 1.6 !important;
|
||||
color: #ffffff !important;
|
||||
background: transparent !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box !important;
|
||||
resize: none;
|
||||
overflow-y: auto !important;
|
||||
caret-color: #64ffda !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.category-selectors {
|
||||
@@ -569,10 +591,16 @@ body.high-contrast :host ::ng-deep {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-bottom: 1.8rem;
|
||||
position: relative;
|
||||
.lyrics-view {
|
||||
max-width: 900px;
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
|
||||
&.chorus {
|
||||
background: rgba(var(--ion-color-secondary-rgb), 0.03);
|
||||
border-left: 3px solid var(--ion-color-secondary);
|
||||
@@ -581,25 +609,29 @@ body.high-contrast :host ::ng-deep {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.preview-section-label {
|
||||
.section-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--ion-color-secondary);
|
||||
margin-bottom: 0.4rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.7;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-lyric-line {
|
||||
margin-bottom: 0.8rem;
|
||||
.lyric-line {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
min-height: 1.5em;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preview-chord-segment {
|
||||
.chord-segment {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
vertical-align: bottom;
|
||||
@@ -609,16 +641,16 @@ body.high-contrast :host ::ng-deep {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.preview-chord {
|
||||
font-size: 0.78em;
|
||||
.chord {
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
color: var(--ion-color-secondary);
|
||||
height: 1.25em;
|
||||
height: 1.2em;
|
||||
margin-bottom: -0.2em;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.preview-seg-text {
|
||||
.seg-text {
|
||||
white-space: pre;
|
||||
&::after {
|
||||
content: '\200b';
|
||||
@@ -654,6 +686,16 @@ body.high-contrast :host ::ng-deep {
|
||||
--color: #000000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preview-transpose-group {
|
||||
border-color: #000000 !important;
|
||||
span {
|
||||
color: #000000 !important;
|
||||
}
|
||||
ion-button {
|
||||
--color: #000000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
@@ -672,23 +714,23 @@ body.high-contrast :host ::ng-deep {
|
||||
}
|
||||
|
||||
.preview-lyrics-container {
|
||||
.preview-section {
|
||||
.section {
|
||||
&.chorus {
|
||||
background: #f5f5f5;
|
||||
border-left: 3px solid #000000;
|
||||
}
|
||||
|
||||
.preview-section-label {
|
||||
.section-label {
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-lyric-line {
|
||||
.lyric-line {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.preview-chord-segment {
|
||||
.preview-chord {
|
||||
.chord-segment {
|
||||
.chord {
|
||||
color: #000000;
|
||||
text-decoration: underline;
|
||||
font-weight: 800;
|
||||
@@ -703,6 +745,17 @@ body.high-contrast :host ::ng-deep {
|
||||
}
|
||||
}
|
||||
|
||||
.youtube-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.youtube-input-row {
|
||||
ion-button {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* CUSTOM STYLES FOR INTUITIVE MOBILE RESPONSIVENESS */
|
||||
.custom-mobile-segment {
|
||||
display: none;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ViewChild, ElementRef, inject } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { IonicModule, ToastController, IonTextarea, PopoverController, NavController, AlertController } from '@ionic/angular';
|
||||
@@ -9,6 +9,7 @@ import { PlaylistService } from '../../services/playlist.service';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
|
||||
import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service';
|
||||
import { YoutubePlayerService } from '../../services/youtube-player.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-propose-canto',
|
||||
@@ -17,8 +18,9 @@ import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, IonicModule, RouterModule]
|
||||
})
|
||||
export class ProposeCantoPage implements OnInit {
|
||||
@ViewChild('contentTextarea', { static: false }) contentTextarea!: IonTextarea;
|
||||
export class ProposeCantoPage implements OnInit, OnDestroy {
|
||||
@ViewChild('nativeTextarea', { static: false }) nativeTextarea!: ElementRef<HTMLTextAreaElement>;
|
||||
|
||||
@ViewChild('cameraInput', { static: false }) cameraInput!: ElementRef;
|
||||
@ViewChild('fileInput', { static: false }) fileInput!: ElementRef;
|
||||
|
||||
@@ -31,12 +33,154 @@ export class ProposeCantoPage implements OnInit {
|
||||
private router = inject(Router);
|
||||
public lyricsParser = inject(LyricsParserService);
|
||||
private alertCtrl = inject(AlertController);
|
||||
public youtubePlayerService = inject(YoutubePlayerService);
|
||||
|
||||
showChordsPreview: boolean = true;
|
||||
activeTab: string = 'editor';
|
||||
transposeAmount: number = 0;
|
||||
|
||||
get highlightedHtml(): string {
|
||||
if (!this.content) return '';
|
||||
|
||||
let escaped = this.content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
escaped = escaped.replace(/\[([^\]]+)\]/g, (match, chord) => {
|
||||
const transposed = this.transposeAmount !== 0
|
||||
? this.lyricsParser.transposeChord(chord, this.transposeAmount)
|
||||
: chord;
|
||||
return `<span class="editor-chord">[${transposed}]</span>`;
|
||||
});
|
||||
|
||||
const lines = escaped.split('\n');
|
||||
let htmlLines: string[] = [];
|
||||
let inChorus = false;
|
||||
let inVerse = false;
|
||||
let inVerseNum = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
let currentInChorus = inChorus;
|
||||
let currentInVerse = inVerse;
|
||||
let currentInVerseNum = inVerseNum;
|
||||
|
||||
let isTagLine = false;
|
||||
let processedContent = line;
|
||||
|
||||
if (trimmed === '{start_chorus}' || trimmed === '{soc}') {
|
||||
inChorus = true;
|
||||
currentInChorus = true;
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
} else if (trimmed === '{end_chorus}' || trimmed === '{eoc}') {
|
||||
inChorus = false;
|
||||
currentInChorus = true;
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
} else if (trimmed === '{start_verse}' || trimmed === '{sov}') {
|
||||
inVerse = true;
|
||||
currentInVerse = true;
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
} else if (trimmed === '{end_verse}' || trimmed === '{eov}') {
|
||||
inVerse = false;
|
||||
currentInVerse = true;
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
} else if (trimmed === '{start_verse_num}') {
|
||||
inVerseNum = true;
|
||||
currentInVerseNum = true;
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
} else if (trimmed === '{end_verse_num}') {
|
||||
inVerseNum = false;
|
||||
currentInVerseNum = true;
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
} else if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||||
isTagLine = true;
|
||||
processedContent = `<span class="editor-tag">${line}</span>`;
|
||||
}
|
||||
|
||||
let classes = ['editor-line'];
|
||||
if (currentInChorus) classes.push('chorus');
|
||||
if (currentInVerse) classes.push('verse');
|
||||
if (currentInVerseNum) classes.push('verse-num');
|
||||
if (isTagLine) classes.push('tag-line');
|
||||
|
||||
const finalContent = processedContent || '​';
|
||||
htmlLines.push(`<div class="${classes.join(' ')}">${finalContent}</div>`);
|
||||
}
|
||||
|
||||
return htmlLines.join('\n');
|
||||
}
|
||||
|
||||
|
||||
|
||||
get parsedSections(): ParsedSection[] {
|
||||
return this.lyricsParser.parseAccordi(this.content);
|
||||
const sections = this.lyricsParser.parseAccordi(this.content);
|
||||
if (this.showChordsPreview && this.transposeAmount !== 0) {
|
||||
return this.lyricsParser.transposeSections(sections, this.transposeAmount);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
transposeUp() {
|
||||
this.transposeAmount = (this.transposeAmount + 1) > 12 ? -11 : this.transposeAmount + 1;
|
||||
}
|
||||
|
||||
transposeDown() {
|
||||
this.transposeAmount = (this.transposeAmount - 1) < -12 ? 11 : this.transposeAmount - 1;
|
||||
}
|
||||
|
||||
setDefaultTonalita() {
|
||||
if (this.transposeAmount === 0 || !this.content) return;
|
||||
const chordRegex = /\[([^\]]+)\]/g;
|
||||
this.content = this.content.replace(chordRegex, (match, chord) => {
|
||||
const transposed = this.lyricsParser.transposeChord(chord, this.transposeAmount);
|
||||
return `[${transposed}]`;
|
||||
});
|
||||
this.transposeAmount = 0;
|
||||
}
|
||||
|
||||
isAudioLoaded(): boolean {
|
||||
return !!this.youtubeLink && this.cantiService.getYoutubeId(this.youtubeLink) !== null;
|
||||
}
|
||||
|
||||
loadedYoutubeLink: string = '';
|
||||
|
||||
loadEditorAudio() {
|
||||
if (!this.youtubeLink) return;
|
||||
const videoId = this.cantiService.getYoutubeId(this.youtubeLink);
|
||||
if (videoId) {
|
||||
this.youtubePlayerService.initPlayer(this.editId || 'editing_song', 0, undefined, undefined, this.youtubeLink);
|
||||
this.loadedYoutubeLink = this.youtubeLink;
|
||||
}
|
||||
}
|
||||
|
||||
togglePlayPause() {
|
||||
const currentId = this.editId || 'editing_song';
|
||||
const isLoaded = this.youtubePlayerService.currentCantoId() === currentId && this.loadedYoutubeLink === this.youtubeLink;
|
||||
if (!isLoaded && this.youtubeLink) {
|
||||
this.loadEditorAudio();
|
||||
} else {
|
||||
this.youtubePlayerService.togglePlayPause();
|
||||
}
|
||||
}
|
||||
|
||||
onSeek(event: any) {
|
||||
const value = event.detail.value;
|
||||
this.youtubePlayerService.seekTo(value);
|
||||
}
|
||||
|
||||
formatSeconds(seconds: number): string {
|
||||
if (isNaN(seconds)) return '0:00';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
||||
}
|
||||
|
||||
toggleChordsPreview() {
|
||||
@@ -164,8 +308,12 @@ export class ProposeCantoPage implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.youtubePlayerService.stop();
|
||||
}
|
||||
|
||||
async insertText(tag: string) {
|
||||
const input = await this.contentTextarea.getInputElement();
|
||||
const input = this.nativeTextarea.nativeElement;
|
||||
const start = input.selectionStart || 0;
|
||||
const end = input.selectionEnd || 0;
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ export class FaceDetectorService {
|
||||
// Gesture state machine
|
||||
private tiltStartTime: number = 0;
|
||||
private inCooldown: boolean = false;
|
||||
private lastTriggerTime: number = 0;
|
||||
private readonly TILT_THRESHOLD = 15; // Degrees to trigger next/prev page
|
||||
private readonly TILT_HOLD_MS = 300; // How long to hold the tilt
|
||||
private readonly RETURN_THRESHOLD = 6; // Degrees to reset cooldown
|
||||
private readonly TRIGGER_COOLDOWN_MS = 2000; // Minimum time between consecutive gestures in ms
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -33,12 +35,10 @@ export class FaceDetectorService {
|
||||
}
|
||||
|
||||
const cameraScript = document.createElement('script');
|
||||
cameraScript.src = 'https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js';
|
||||
cameraScript.crossOrigin = 'anonymous';
|
||||
cameraScript.src = 'assets/mediapipe/camera_utils.js';
|
||||
|
||||
const faceMeshScript = document.createElement('script');
|
||||
faceMeshScript.src = 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js';
|
||||
faceMeshScript.crossOrigin = 'anonymous';
|
||||
faceMeshScript.src = 'assets/mediapipe/face_mesh.js';
|
||||
|
||||
cameraScript.onload = () => {
|
||||
document.head.appendChild(faceMeshScript);
|
||||
@@ -88,7 +88,7 @@ export class FaceDetectorService {
|
||||
}
|
||||
|
||||
this.faceMesh = new FaceMeshLib({
|
||||
locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
|
||||
locateFile: (file: string) => `assets/mediapipe/${file}`
|
||||
});
|
||||
|
||||
this.faceMesh.setOptions({
|
||||
@@ -155,7 +155,7 @@ export class FaceDetectorService {
|
||||
if (absAngle > this.TILT_THRESHOLD) {
|
||||
this.isTilted.set(true);
|
||||
|
||||
if (!this.inCooldown) {
|
||||
if (!this.inCooldown && (Date.now() - this.lastTriggerTime > this.TRIGGER_COOLDOWN_MS)) {
|
||||
if (this.tiltStartTime === 0) {
|
||||
this.tiltStartTime = Date.now();
|
||||
} else if (Date.now() - this.tiltStartTime > this.TILT_HOLD_MS) {
|
||||
@@ -167,6 +167,7 @@ export class FaceDetectorService {
|
||||
}
|
||||
this.inCooldown = true;
|
||||
this.tiltStartTime = 0;
|
||||
this.lastTriggerTime = Date.now();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -189,6 +190,7 @@ export class FaceDetectorService {
|
||||
this.isTilted.set(false);
|
||||
this.inCooldown = false;
|
||||
this.tiltStartTime = 0;
|
||||
this.lastTriggerTime = 0;
|
||||
|
||||
if (this.camera) {
|
||||
try {
|
||||
|
||||
@@ -17,17 +17,21 @@ export class MyCantiService {
|
||||
|
||||
private _storage: Storage | null = null;
|
||||
public myCanti = signal<Canto[]>([]);
|
||||
private initPromise!: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
this.initPromise = this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
async init(): Promise<void> {
|
||||
this._storage = this.cantiService.getStorage();
|
||||
if (!this._storage) {
|
||||
// If CantiService hasn't initialized storage yet, wait a bit
|
||||
setTimeout(() => this.init(), 500);
|
||||
return;
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await this.init();
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
const saved = await this._storage.get('my-canti');
|
||||
if (saved) {
|
||||
@@ -36,6 +40,7 @@ export class MyCantiService {
|
||||
}
|
||||
|
||||
async saveCanto(canto: Partial<Canto>): Promise<Canto> {
|
||||
await this.initPromise;
|
||||
const current = this.myCanti();
|
||||
let updated: Canto[];
|
||||
let targetCanto: Canto;
|
||||
@@ -108,6 +113,7 @@ export class MyCantiService {
|
||||
}
|
||||
|
||||
async deleteCanto(id: string) {
|
||||
await this.initPromise;
|
||||
const updated = this.myCanti().filter(c => c.id !== id);
|
||||
this.myCanti.set(updated);
|
||||
await this._storage?.set('my-canti', updated);
|
||||
|
||||
@@ -33,6 +33,7 @@ export class PlaylistService {
|
||||
public remotePlaylist = signal<any | null>(null);
|
||||
public remoteCustomSongs = signal<any[]>([]);
|
||||
public remoteShareCanti = signal<Canto[]>([]);
|
||||
public hasRemotePlaylistUpdate = signal<boolean>(false);
|
||||
|
||||
private _storage: Storage | null = null;
|
||||
private initPromise!: Promise<void>;
|
||||
@@ -43,10 +44,7 @@ export class PlaylistService {
|
||||
return [];
|
||||
}
|
||||
return this.comunitaService.comunitaScalette().map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
ids: s.ids,
|
||||
createdAt: s.date,
|
||||
...s,
|
||||
isComunita: true
|
||||
}));
|
||||
});
|
||||
@@ -83,21 +81,23 @@ export class PlaylistService {
|
||||
|
||||
getPlaylistsStorageKey(): string {
|
||||
const code = this.comunitaService.comunitaCode();
|
||||
const isCommunityActive = this.comunitaService.isFilterActive();
|
||||
if (code && isCommunityActive) {
|
||||
return `playlists_comunita_${code}`;
|
||||
const active = this.comunitaService.isFilterActive();
|
||||
if (code && active) {
|
||||
return `playlists_${code}`;
|
||||
}
|
||||
return 'playlists';
|
||||
}
|
||||
|
||||
async init() {
|
||||
const storage = await this.storage.create();
|
||||
this._storage = storage;
|
||||
await this.loadPlaylistsForCurrentContext();
|
||||
}
|
||||
|
||||
async loadPlaylistsForCurrentContext() {
|
||||
if (!this._storage) return;
|
||||
async init(): Promise<void> {
|
||||
this._storage = this.cantiService.getStorage();
|
||||
if (!this._storage) {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await this.init();
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
const key = this.getPlaylistsStorageKey();
|
||||
const saved = await this._storage.get(key);
|
||||
this.playlists.set(saved || []);
|
||||
@@ -126,6 +126,73 @@ export class PlaylistService {
|
||||
}
|
||||
}
|
||||
|
||||
async loadPlaylistsForCurrentContext() {
|
||||
await this.initPromise;
|
||||
const key = this.getPlaylistsStorageKey();
|
||||
const saved = await this._storage?.get(key);
|
||||
this.playlists.set(saved || []);
|
||||
|
||||
const lastKey = `lastPlaylist_${key}`;
|
||||
const last = await this._storage?.get(lastKey);
|
||||
this.lastPlaylist.set(last || null);
|
||||
|
||||
// Carica la playlist remota persistita e i relativi canti personalizzati
|
||||
const remotePl = await this._storage?.get('remote_playlist');
|
||||
if (remotePl) {
|
||||
this.remotePlaylist.set(remotePl);
|
||||
}
|
||||
const remoteSongs = await this._storage?.get('remote_custom_songs');
|
||||
if (remoteSongs) {
|
||||
this.remoteCustomSongs.set(remoteSongs);
|
||||
}
|
||||
const remoteShareSongs = await this._storage?.get('remote_share_canti');
|
||||
if (remoteShareSongs) {
|
||||
this.remoteShareCanti.set(remoteShareSongs);
|
||||
}
|
||||
|
||||
// Aggiorna in background la playlist remota per sincronizzare eventuali modifiche
|
||||
if (remotePl) {
|
||||
this.refreshRemotePlaylist();
|
||||
}
|
||||
}
|
||||
|
||||
async checkForRemotePlaylistUpdates() {
|
||||
const remotePl = this.remotePlaylist();
|
||||
if (!remotePl) {
|
||||
this.hasRemotePlaylistUpdate.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = remotePl.id.split('_');
|
||||
if (parts.length < 3) return;
|
||||
const uid = parts[1];
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.canticristiani.it/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
|
||||
if (!response.ok) return;
|
||||
const remoteJson = await response.json();
|
||||
if (Array.isArray(remoteJson)) {
|
||||
const playlists = remoteJson.filter((item: any) => item.momenti && item.momenti.includes('Playlist'));
|
||||
const updatedPl = playlists.find((p: any) => `remote_${uid}_${p.id_canti}` === remotePl.id);
|
||||
if (updatedPl) {
|
||||
const serverIds = updatedPl.testo ? updatedPl.testo.split(',') : [];
|
||||
const localIds = remotePl.ids || [];
|
||||
|
||||
const idsMatch = serverIds.length === localIds.length && serverIds.every((id: string, idx: number) => id === localIds[idx]);
|
||||
const nameMatch = `[Remote] ${updatedPl.titolo}` === remotePl.name;
|
||||
|
||||
if (!idsMatch || !nameMatch) {
|
||||
this.hasRemotePlaylistUpdate.set(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.hasRemotePlaylistUpdate.set(false);
|
||||
} catch (e) {
|
||||
console.warn('Failed to check for remote playlist updates:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshRemotePlaylist() {
|
||||
const remotePl = this.remotePlaylist();
|
||||
if (!remotePl) return;
|
||||
@@ -177,6 +244,12 @@ export class PlaylistService {
|
||||
this.remoteCustomSongs.set(customSongs);
|
||||
await this._storage?.set('remote_playlist', updatedPl);
|
||||
await this._storage?.set('remote_custom_songs', customSongs);
|
||||
this.hasRemotePlaylistUpdate.set(false);
|
||||
|
||||
if (this.activePlaylistId() === remotePl.id) {
|
||||
this.activeListIds.set(updatedPl.ids);
|
||||
this.activeListName.set(updatedPl.name);
|
||||
}
|
||||
console.log('[Remote-Sync] Remote playlist and custom songs updated successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +106,8 @@ export class YoutubePlayerService {
|
||||
};
|
||||
}
|
||||
|
||||
public initPlayer(cantoId: string, startTime: number = 0, onEnded?: () => void, onError?: () => void) {
|
||||
if (this.currentCantoId() === cantoId && this.player) {
|
||||
public initPlayer(cantoId: string, startTime: number = 0, onEnded?: () => void, onError?: () => void, customYoutubeLink?: string) {
|
||||
if (this.currentCantoId() === cantoId && this.player && !customYoutubeLink) {
|
||||
this.onEndedCallback = onEnded || null;
|
||||
this.onErrorCallback = onError || null;
|
||||
|
||||
@@ -123,36 +123,55 @@ export class YoutubePlayerService {
|
||||
this.onEndedCallback = onEnded || null;
|
||||
this.onErrorCallback = onError || null;
|
||||
|
||||
let canto = this.cantiService.getCantoById(cantoId);
|
||||
if (!canto) {
|
||||
canto = this.myCantiService.myCanti().find(c => c.id === cantoId);
|
||||
let videoId: string | null = null;
|
||||
if (customYoutubeLink) {
|
||||
videoId = this.cantiService.getYoutubeId(customYoutubeLink);
|
||||
} else {
|
||||
let canto = this.cantiService.getCantoById(cantoId);
|
||||
if (!canto) {
|
||||
canto = this.myCantiService.myCanti().find(c => c.id === cantoId);
|
||||
}
|
||||
if (!canto) {
|
||||
canto = this.comunitaService.comunitaCantiPersonali().find(c => c.id === cantoId);
|
||||
}
|
||||
if (!canto) {
|
||||
canto = this.playlistService.remoteCustomSongs().find(c => c.id === cantoId || String(c.id_canti) === cantoId);
|
||||
}
|
||||
if (!canto) {
|
||||
canto = this.playlistService.remoteShareCanti().find(c => c.id === cantoId || String(c.id_canti) === cantoId);
|
||||
}
|
||||
if (canto) {
|
||||
videoId = this.cantiService.getYoutubeId(canto.link_youtube);
|
||||
}
|
||||
}
|
||||
if (!canto) {
|
||||
canto = this.comunitaService.comunitaCantiPersonali().find(c => c.id === cantoId);
|
||||
}
|
||||
if (!canto) {
|
||||
canto = this.playlistService.remoteCustomSongs().find(c => c.id === cantoId || String(c.id_canti) === cantoId);
|
||||
}
|
||||
if (!canto) {
|
||||
canto = this.playlistService.remoteShareCanti().find(c => c.id === cantoId || String(c.id_canti) === cantoId);
|
||||
}
|
||||
if (!canto) return;
|
||||
|
||||
const videoId = this.cantiService.getYoutubeId(canto.link_youtube);
|
||||
if (!videoId) {
|
||||
if (onError) onError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(window as any).YT || !(window as any).YT.Player) {
|
||||
setTimeout(() => this.initPlayer(cantoId, startTime, onEnded, onError), 200);
|
||||
setTimeout(() => this.initPlayer(cantoId, startTime, onEnded, onError, customYoutubeLink), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse existing player if possible
|
||||
if (this.player && this.player.loadVideoById) {
|
||||
this.currentCantoId.set(cantoId);
|
||||
this.mediaSessionService.updateMetadata(cantoId);
|
||||
if (customYoutubeLink) {
|
||||
if ('mediaSession' in navigator && 'MediaMetadata' in window) {
|
||||
try {
|
||||
(navigator as any).mediaSession.metadata = new (window as any).MediaMetadata({
|
||||
title: 'Anteprima Canto',
|
||||
artist: 'Canti Cristiani',
|
||||
album: 'Canti Cristiani',
|
||||
artwork: [{ src: 'assets/icon/favicon.png', sizes: '512x512', type: 'image/png' }]
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.mediaSessionService.updateMetadata(cantoId);
|
||||
}
|
||||
this.player.loadVideoById({
|
||||
videoId: videoId,
|
||||
startSeconds: startTime
|
||||
@@ -163,7 +182,20 @@ export class YoutubePlayerService {
|
||||
|
||||
this.destroyPlayer();
|
||||
this.currentCantoId.set(cantoId);
|
||||
this.mediaSessionService.updateMetadata(cantoId);
|
||||
if (customYoutubeLink) {
|
||||
if ('mediaSession' in navigator && 'MediaMetadata' in window) {
|
||||
try {
|
||||
(navigator as any).mediaSession.metadata = new (window as any).MediaMetadata({
|
||||
title: 'Anteprima Canto',
|
||||
artist: 'Canti Cristiani',
|
||||
album: 'Canti Cristiani',
|
||||
artwork: [{ src: 'assets/icon/favicon.png', sizes: '512x512', type: 'image/png' }]
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.mediaSessionService.updateMetadata(cantoId);
|
||||
}
|
||||
|
||||
this.player = new (window as any).YT.Player('global-yt-player-container', {
|
||||
height: '1',
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const VERSION = '2026.06.18.1434';
|
||||
export const VERSION = '2026.06.27.1457';
|
||||
|
||||
Reference in New Issue
Block a user