Compare commits
8 Commits
333858bf48
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 171780bf87 | |||
| a15a6c2139 | |||
| 0069c1a55f | |||
| ad9286002b | |||
| 73097bf9bd | |||
| 3c91355e10 | |||
| 94d014f815 | |||
| 9f9b164937 |
@@ -0,0 +1,3 @@
|
|||||||
|
# Regole del Progetto
|
||||||
|
|
||||||
|
Quando trovi dei trattini verticali `|` tra gli accordi, ricordati di inserirli all'interno della riga degli accordi (cioè racchiusi tra parentesi quadre, ad esempio come accordo `[|]`), non come testo normale.
|
||||||
+13
-39
@@ -8,41 +8,14 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Configurazione Target e Parallelismo ---
|
# --- Configurazione Target ---
|
||||||
TARGET="tophost"
|
TARGET="contabo"
|
||||||
THREADS=""
|
|
||||||
|
|
||||||
if [ "$1" = "contabo" ] || [ "$1" = "tophost" ]; then
|
|
||||||
TARGET="$1"
|
|
||||||
THREADS="$2"
|
|
||||||
else
|
|
||||||
# Se il primo argomento è un numero, indica il numero di thread per tophost
|
|
||||||
if [[ "$1" =~ ^[0-9]+$ ]]; then
|
|
||||||
THREADS="$1"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
THREADS=${THREADS:-${FTP_THREADS:-100}}
|
|
||||||
export FTP_THREADS=$THREADS
|
|
||||||
|
|
||||||
echo "🎯 Target di deploy: $TARGET"
|
echo "🎯 Target di deploy: $TARGET"
|
||||||
|
|
||||||
# --- Validazione configurazione basata sul target ---
|
# --- Validazione configurazione ---
|
||||||
if [ "$TARGET" = "tophost" ]; then
|
if [ -z "$VPS_HOST" ] || [ -z "$VPS_USER" ] || [ -z "$VPS_PATH" ]; then
|
||||||
FTP_HOST="ftp.canticristiani.it"
|
echo "❌ Errore: VPS_HOST, VPS_USER o VPS_PATH non definiti nel file .env"
|
||||||
FTP_USER="canticristiani.it"
|
exit 1
|
||||||
FTP_PASS="$FTP_PASSWORD"
|
|
||||||
REMOTE_DIR=""
|
|
||||||
|
|
||||||
if [ -z "$FTP_PASS" ]; then
|
|
||||||
echo "❌ Errore: FTP_PASSWORD non definita nel file .env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ -z "$VPS_HOST" ] || [ -z "$VPS_USER" ] || [ -z "$VPS_PATH" ]; then
|
|
||||||
echo "❌ Errore: VPS_HOST, VPS_USER o VPS_PATH non definiti nel file .env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Aggiornamento Versione ---
|
# --- Aggiornamento Versione ---
|
||||||
@@ -84,10 +57,11 @@ echo "{\"version\":\"$VERSION\",\"buildTime\":$BUILD_TIMESTAMP}" > www/version.j
|
|||||||
echo "📄 Generato version.json: v$VERSION (timestamp: $BUILD_TIMESTAMP)"
|
echo "📄 Generato version.json: v$VERSION (timestamp: $BUILD_TIMESTAMP)"
|
||||||
|
|
||||||
# --- Upload ---
|
# --- Upload ---
|
||||||
if [ "$TARGET" = "tophost" ]; then
|
echo "🚀 Upload via SSH/rsync su Contabo ($VPS_HOST) in corso..."
|
||||||
echo "🚀 Upload via FTP su Tophost in corso..."
|
rsync -avz --delete --exclude 'api' www/ "$VPS_USER@$VPS_HOST:$VPS_PATH"
|
||||||
python3 scratch/deploy_ftp.py
|
|
||||||
else
|
# --- Allineamento Cantiletture JSON ---
|
||||||
echo "🚀 Upload via SSH/rsync su Contabo ($VPS_HOST) in corso..."
|
if [ -f "./allinealetture.sh" ]; then
|
||||||
rsync -avz --delete --exclude 'api' www/ "$VPS_USER@$VPS_HOST:$VPS_PATH"
|
echo "🔄 Sincronizzazione cantiletture.json su server..."
|
||||||
|
./allinealetture.sh || echo "⚠️ Warning: Allineamento cantiletture.json fallito"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# Parametri letti da canti.json e da Comunità
|
||||||
|
|
||||||
|
Questo documento elenca tutti i parametri e i campi strutturati che vengono letti, elaborati e salvati dall'applicazione a partire dal file globale `canti.json` e dalle API/file di configurazione delle **Comunità**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Parametri da `canti.json`
|
||||||
|
|
||||||
|
Il file `canti.json` rappresenta il database globale dei canti dell'applicazione. Viene scaricato dall'endpoint configurato in `CantiService` (es. `https://www.canticristiani.it/api/canti.json`).
|
||||||
|
|
||||||
|
### Struttura Principale del File
|
||||||
|
|
||||||
|
Il JSON restituito contiene diversi nodi chiave, ciascuno contenente un array `data`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"canti": { "data": [...] },
|
||||||
|
"indice_liturgico": { "data": [...] },
|
||||||
|
"indice_tematico": { "data": [...] },
|
||||||
|
"tema": { "data": [...] },
|
||||||
|
"canti_eseguiti": { "data": [...] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Nodo `canti.data` (Lista dei Canti)
|
||||||
|
Ciascun elemento rappresenta un canto e viene mappato nell'interfaccia `Canto`:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_canti` | `number` | Identificativo univoco del canto (usato internamente anche come stringa `id`). |
|
||||||
|
| `titolo` | `string` | Titolo del canto. |
|
||||||
|
| `testo` | `string` | Testo del canto (può includere indicazioni di accordi). |
|
||||||
|
| `accordi` | `string` (opzionale) | Accordi musicali associati al canto. |
|
||||||
|
| `autore` | `string` (opzionale) | Autore o autori del canto. |
|
||||||
|
| `link_youtube` | `string` (opzionale) | URL o ID del video di YouTube associato al canto. |
|
||||||
|
| `data_update` | `string` (opzionale) | Data dell'ultimo aggiornamento (formato `YYYY-MM-DD HH:mm:ss`, formattata a schermo in `DD/MM/YYYY`). |
|
||||||
|
| `nonValidato` | `boolean` (opzionale) | Indica se il canto è in attesa di validazione. |
|
||||||
|
| `isPersonal` | `boolean` (opzionale) | Flag locale per identificare se si tratta di un canto personale dell'utente. |
|
||||||
|
|
||||||
|
*Nota: Durante l'importazione, viene aggiunto un array `id_momenti: number[]` ricavato dalla tabella pivot `tema.data`.*
|
||||||
|
|
||||||
|
#### Nodo `indice_liturgico.data` e `indice_tematico.data` (Indici/Tag)
|
||||||
|
Mappati nell'interfaccia `Indice`:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_indice_liturgico` / `id_indice_tematico` | `number` | Identificativo dell'indice/momento. |
|
||||||
|
| `tag_name` | `string` | Nome visualizzato del tag (es. "Ingresso", "Offertorio"). |
|
||||||
|
| `slug` | `string` | Versione ottimizzata per URL del tag. |
|
||||||
|
|
||||||
|
*Nota: A livello applicativo viene aggiunto il campo `type` con valore `'liturgico'` o `'tematico'`.*
|
||||||
|
|
||||||
|
#### Nodo `tema.data` (Relazione Pivot Canti-Indici)
|
||||||
|
Utilizzato per associare a ogni canto i rispettivi momenti liturgici o tematici:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_canti` | `number` | ID del canto associato. |
|
||||||
|
| `id_momento` | `number` | ID del momento liturgico/tematico. |
|
||||||
|
|
||||||
|
#### Nodo `canti_eseguiti.data`
|
||||||
|
Informazioni sull'esecuzione dei canti:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_canti` | `number` | ID del canto eseguito. |
|
||||||
|
| `num` | `number` | Numero di esecuzioni o indicatore di frequenza. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Parametri dalle Comunità
|
||||||
|
|
||||||
|
I dati di una comunità vengono letti in due modi: tramite l'API di produzione (v3) oppure tramite un file JSON statico di fallback.
|
||||||
|
|
||||||
|
### Opzione A: API di Produzione (`get_all_app_tables`)
|
||||||
|
Endpoint: `https://libretto.mmcinet.eu/canti/api/v3/get_all_app_tables`
|
||||||
|
|
||||||
|
L'API risponde con un oggetto contenente diverse tabelle relazionali:
|
||||||
|
|
||||||
|
#### 1. `parrocchia.data` (Dettagli della Comunità)
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_parrocchia` | `number` | ID interno della parrocchia/comunità. |
|
||||||
|
| `nome` | `string` | Nome della comunità (es. "Parrocchia S. Maria"). |
|
||||||
|
| `codice` | `string` | Codice alfanumerico della comunità. |
|
||||||
|
| `mail` | `string` | Email di riferimento della comunità. |
|
||||||
|
| `guid_parrocchia` | `string` | GUID univoco. |
|
||||||
|
|
||||||
|
#### 2. `parrocchia_canti.data` (Associazione Canti-Comunità)
|
||||||
|
Indica quali canti del database generale appartengono al repertorio della comunità:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_parrocchia` | `number` | ID della parrocchia. |
|
||||||
|
| `id_canti` | `number` | ID del canto associato. |
|
||||||
|
| `num_canto` | `number` | Numero progressivo o di classificazione del canto all'interno della comunità. |
|
||||||
|
|
||||||
|
#### 3. `canti_settings.data` (Impostazioni di Esecuzione personalizzate)
|
||||||
|
Configurazioni specifiche per l'esecuzione del canto in comunità:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_canti` | `number` | ID del canto. |
|
||||||
|
| `speed` | `number` | Velocità di scorrimento (auto-scroll) consigliata. |
|
||||||
|
| `tonalita` | `number` | Semitoni di trasposizione (trasporto tonalità) consigliati. |
|
||||||
|
|
||||||
|
#### 4. `canti_personali.data` (Canti personalizzati/inediti della Comunità)
|
||||||
|
Canti inseriti direttamente dalla comunità e non presenti nel database globale:
|
||||||
|
|
||||||
|
| Parametro Originale | Tipo | Descrizione / Mapping Applicativo |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_canti` | `number` | ID del canto personale. |
|
||||||
|
| `titolo` | `string` | Titolo del canto. |
|
||||||
|
| `accordi` | `string` | Testo con gli accordi del canto. |
|
||||||
|
| `autore` | `string` | Autore del canto. |
|
||||||
|
| `link_youtube` | `string` | Link YouTube. |
|
||||||
|
| `data_update` | `string` | Data dell'ultimo aggiornamento. |
|
||||||
|
| `stato` | `number` | Se uguale a `10`, il canto viene contrassegnato come `nonValidato = true`. |
|
||||||
|
|
||||||
|
*Nota: Vengono impostati automaticamente `isPersonal = true` e `id_momenti = []`.*
|
||||||
|
|
||||||
|
#### 5. Scalette della Comunità (`lista_nome` + `lista_esecuzione`)
|
||||||
|
L'applicazione ricostruisce l'elenco delle scalette (`ComunitaScaletta`):
|
||||||
|
* **`lista_nome.data`** (Testate delle scalette):
|
||||||
|
* `id_lista` (`string`/`number`): ID della scaletta.
|
||||||
|
* `nome` (`string`): Nome della scaletta (es. "Domenica delle Palme").
|
||||||
|
* `progr` (`string`): Data o stringa di ordinamento (mappata in `date`).
|
||||||
|
* **`lista_esecuzione.data`** (Canti contenuti nelle scalette):
|
||||||
|
* `id_lista` (`string`/`number`): Associazione alla scaletta.
|
||||||
|
* `id_canti` (`number`): ID del canto.
|
||||||
|
* `progr` (`number`): Ordine progressivo del canto all'interno della scaletta (usato per l'ordinamento).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Opzione B: Fallback Statico (`comunita_[codice].json`)
|
||||||
|
Se l'API di produzione non è raggiungibile, viene tentato il download di un file statico (es. `comunita_123456.json`) dall'origine del sito.
|
||||||
|
|
||||||
|
La struttura attesa per questo file è molto più semplice:
|
||||||
|
|
||||||
|
| Parametro JSON | Tipo | Descrizione |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `id_comunita` | `string` | Codice identificativo della comunità. |
|
||||||
|
| `nome_comunita` | `string` | Nome leggibile della comunità. |
|
||||||
|
| `canti` | `(number \| string)[]` | Array contenente gli ID di tutti i canti associati a questa comunità. |
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function walkDir(dir, callback) {
|
||||||
|
fs.readdirSync(dir).forEach(f => {
|
||||||
|
let dirPath = path.join(dir, f);
|
||||||
|
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||||
|
if (isDirectory) {
|
||||||
|
walkDir(dirPath, callback);
|
||||||
|
} else {
|
||||||
|
callback(dirPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = new Set();
|
||||||
|
|
||||||
|
walkDir(path.join(__dirname, '../src/app'), (filePath) => {
|
||||||
|
if (filePath.endsWith('.html') || filePath.endsWith('.ts')) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
// Match name="icon-name"
|
||||||
|
const nameMatches = content.matchAll(/name=["']([a-zA-Z0-9-]+)["']/g);
|
||||||
|
for (const match of nameMatches) {
|
||||||
|
icons.add(match[1]);
|
||||||
|
}
|
||||||
|
// Match [name]="... ? 'icon-a' : 'icon-b'"
|
||||||
|
const ternaryMatches = content.matchAll(/'([a-zA-Z0-9-]+-outline|[a-zA-Z0-9-]+-sharp|[a-zA-Z0-9-]+)'/g);
|
||||||
|
for (const match of ternaryMatches) {
|
||||||
|
icons.add(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(Array.from(icons).sort(), null, 2));
|
||||||
+286
-133
@@ -1,13 +1,87 @@
|
|||||||
import { Component, inject, OnInit, signal, effect } from '@angular/core';
|
import { Component, inject, NgZone, OnInit, signal, effect } from '@angular/core';
|
||||||
import { ThemeService } from './services/theme.service';
|
import { ThemeService } from './services/theme.service';
|
||||||
import { CantiService } from './services/canti.service';
|
import { CantiService } from './services/canti.service';
|
||||||
import { SettingsService } from './services/settings.service';
|
import { SettingsService } from './services/settings.service';
|
||||||
import { VERSION } from './version';
|
import { VERSION } from './version';
|
||||||
import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
|
import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
|
||||||
import { ToastController, Platform } from '@ionic/angular';
|
import { ToastController, Platform, AlertController, NavController } from '@ionic/angular';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
||||||
import { filter, first } from 'rxjs/operators';
|
import { filter, first } from 'rxjs/operators';
|
||||||
import { App } from '@capacitor/app';
|
import { App } from '@capacitor/app';
|
||||||
|
import { addIcons } from 'ionicons';
|
||||||
|
import {
|
||||||
|
add,
|
||||||
|
addCircleOutline,
|
||||||
|
addOutline,
|
||||||
|
analyticsOutline,
|
||||||
|
arrowUpCircle,
|
||||||
|
bookOutline,
|
||||||
|
calendarOutline,
|
||||||
|
cameraOutline,
|
||||||
|
cameraReverseOutline,
|
||||||
|
carOutline,
|
||||||
|
chevronBack,
|
||||||
|
chevronBackOutline,
|
||||||
|
chevronDown,
|
||||||
|
chevronDownOutline,
|
||||||
|
chevronForward,
|
||||||
|
chevronForwardOutline,
|
||||||
|
chevronUp,
|
||||||
|
chevronUpOutline,
|
||||||
|
closeCircle,
|
||||||
|
closeCircleOutline,
|
||||||
|
closeOutline,
|
||||||
|
cloudDownloadOutline,
|
||||||
|
cloudOfflineOutline,
|
||||||
|
cloudUploadOutline,
|
||||||
|
contrastOutline,
|
||||||
|
copyOutline,
|
||||||
|
createOutline,
|
||||||
|
documentAttachOutline,
|
||||||
|
documentTextOutline,
|
||||||
|
downloadOutline,
|
||||||
|
eyeOutline,
|
||||||
|
informationCircleOutline,
|
||||||
|
keypadOutline,
|
||||||
|
listOutline,
|
||||||
|
logoApple,
|
||||||
|
logoYoutube,
|
||||||
|
mic,
|
||||||
|
micOutline,
|
||||||
|
musicalNote,
|
||||||
|
musicalNotesOutline,
|
||||||
|
pause,
|
||||||
|
pauseSharp,
|
||||||
|
peopleOutline,
|
||||||
|
personOutline,
|
||||||
|
phoneLandscapeOutline,
|
||||||
|
play,
|
||||||
|
playForwardOutline,
|
||||||
|
playSharp,
|
||||||
|
playSkipBackSharp,
|
||||||
|
playSkipForwardSharp,
|
||||||
|
pricetagsOutline,
|
||||||
|
qrCodeOutline,
|
||||||
|
refreshOutline,
|
||||||
|
remove,
|
||||||
|
removeCircleOutline,
|
||||||
|
removeOutline,
|
||||||
|
reorderTwoOutline,
|
||||||
|
saveOutline,
|
||||||
|
scanOutline,
|
||||||
|
searchOutline,
|
||||||
|
settingsOutline,
|
||||||
|
shareOutline,
|
||||||
|
shareSocialOutline,
|
||||||
|
sparklesOutline,
|
||||||
|
statsChartOutline,
|
||||||
|
swapVerticalOutline,
|
||||||
|
trashOutline,
|
||||||
|
arrowUndoOutline,
|
||||||
|
videocam,
|
||||||
|
videocamOffOutline
|
||||||
|
} from 'ionicons/icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -23,8 +97,12 @@ export class AppComponent implements OnInit {
|
|||||||
private platform = inject(Platform);
|
private platform = inject(Platform);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
|
private location = inject(Location);
|
||||||
|
private navCtrl = inject(NavController);
|
||||||
private toastCtrl = inject(ToastController);
|
private toastCtrl = inject(ToastController);
|
||||||
|
private alertCtrl = inject(AlertController);
|
||||||
private swUpdate = inject(SwUpdate);
|
private swUpdate = inject(SwUpdate);
|
||||||
|
private ngZone = inject(NgZone);
|
||||||
|
|
||||||
public showRedirectOverlay = signal<boolean>(false);
|
public showRedirectOverlay = signal<boolean>(false);
|
||||||
public showInstallOverlay = signal<boolean>(false);
|
public showInstallOverlay = signal<boolean>(false);
|
||||||
@@ -34,11 +112,87 @@ export class AppComponent implements OnInit {
|
|||||||
public redirectFailed = signal<boolean>(false);
|
public redirectFailed = signal<boolean>(false);
|
||||||
public protocolLink = '';
|
public protocolLink = '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Gli aggiornamenti automatici e periodici sono stati rimossi.
|
// Gli aggiornamenti automatici e periodici sono stati rimossi.
|
||||||
// L'aggiornamento viene gestito esclusivamente in modo manuale
|
// L'aggiornamento viene gestito esclusivamente in modo manuale
|
||||||
// tramite il pulsante "Verifica Aggiornamenti App" in SettingsPage.
|
// tramite il pulsante "Verifica Aggiornamenti App" in SettingsPage.
|
||||||
|
|
||||||
|
addIcons({
|
||||||
|
add,
|
||||||
|
'add-circle-outline': addCircleOutline,
|
||||||
|
'add-outline': addOutline,
|
||||||
|
'analytics-outline': analyticsOutline,
|
||||||
|
'arrow-up-circle': arrowUpCircle,
|
||||||
|
'book-outline': bookOutline,
|
||||||
|
'calendar-outline': calendarOutline,
|
||||||
|
'camera-outline': cameraOutline,
|
||||||
|
'camera-reverse-outline': cameraReverseOutline,
|
||||||
|
'car-outline': carOutline,
|
||||||
|
'chevron-back': chevronBack,
|
||||||
|
'chevron-back-outline': chevronBackOutline,
|
||||||
|
'chevron-down': chevronDown,
|
||||||
|
'chevron-down-outline': chevronDownOutline,
|
||||||
|
'chevron-forward': chevronForward,
|
||||||
|
'chevron-forward-outline': chevronForwardOutline,
|
||||||
|
'chevron-up': chevronUp,
|
||||||
|
'chevron-up-outline': chevronUpOutline,
|
||||||
|
'close-circle': closeCircle,
|
||||||
|
'close-circle-outline': closeCircleOutline,
|
||||||
|
'close-outline': closeOutline,
|
||||||
|
'cloud-download-outline': cloudDownloadOutline,
|
||||||
|
'cloud-offline-outline': cloudOfflineOutline,
|
||||||
|
'cloud-upload-outline': cloudUploadOutline,
|
||||||
|
'contrast-outline': contrastOutline,
|
||||||
|
'copy-outline': copyOutline,
|
||||||
|
'create-outline': createOutline,
|
||||||
|
'document-attach-outline': documentAttachOutline,
|
||||||
|
'document-text-outline': documentTextOutline,
|
||||||
|
'download-outline': downloadOutline,
|
||||||
|
'eye-outline': eyeOutline,
|
||||||
|
'information-circle-outline': informationCircleOutline,
|
||||||
|
'keypad-outline': keypadOutline,
|
||||||
|
'list-outline': listOutline,
|
||||||
|
'logo-apple': logoApple,
|
||||||
|
'logo-youtube': logoYoutube,
|
||||||
|
mic,
|
||||||
|
'mic-outline': micOutline,
|
||||||
|
'musical-note': musicalNote,
|
||||||
|
'musical-notes-outline': musicalNotesOutline,
|
||||||
|
pause,
|
||||||
|
'pause-sharp': pauseSharp,
|
||||||
|
'people-outline': peopleOutline,
|
||||||
|
'person-outline': personOutline,
|
||||||
|
'phone-landscape-outline': phoneLandscapeOutline,
|
||||||
|
play,
|
||||||
|
'play-forward-outline': playForwardOutline,
|
||||||
|
'play-sharp': playSharp,
|
||||||
|
'play-skip-back-sharp': playSkipBackSharp,
|
||||||
|
'play-skip-forward-sharp': playSkipForwardSharp,
|
||||||
|
'pricetags-outline': pricetagsOutline,
|
||||||
|
'qr-code-outline': qrCodeOutline,
|
||||||
|
'refresh-outline': refreshOutline,
|
||||||
|
remove,
|
||||||
|
'remove-circle-outline': removeCircleOutline,
|
||||||
|
'remove-outline': removeOutline,
|
||||||
|
'reorder-two-outline': reorderTwoOutline,
|
||||||
|
'save-outline': saveOutline,
|
||||||
|
'scan-outline': scanOutline,
|
||||||
|
'search-outline': searchOutline,
|
||||||
|
'settings-outline': settingsOutline,
|
||||||
|
'share-outline': shareOutline,
|
||||||
|
'share-social-outline': shareSocialOutline,
|
||||||
|
'sparkles-outline': sparklesOutline,
|
||||||
|
'stats-chart-outline': statsChartOutline,
|
||||||
|
'swap-vertical-outline': swapVerticalOutline,
|
||||||
|
'trash-outline': trashOutline,
|
||||||
|
'arrow-undo-outline': arrowUndoOutline,
|
||||||
|
videocam,
|
||||||
|
'videocam-off-outline': videocamOffOutline
|
||||||
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
this.checkLoaderDismissal();
|
this.checkLoaderDismissal();
|
||||||
});
|
});
|
||||||
@@ -62,24 +216,44 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
// Gestione tasto back per PWA/Browser (intercettando popstate di Angular Router)
|
// Gestione del tasto back (PWA/Browser e Nativo/Hardware) secondo le 3 specifiche:
|
||||||
this.router.events.subscribe(event => {
|
// 1- Se siamo su un canto (/player o /display), premendo back andiamo sempre sulla home.
|
||||||
if (event instanceof NavigationStart && event.navigationTrigger === 'popstate') {
|
// 2- Se siamo sulla home (/home o /), premendo back l'app deve uscire.
|
||||||
const targetUrl = event.url.split('?')[0];
|
// 3- Per tutto il resto, segue la logica standard andando alla pagina precedente nello storico.
|
||||||
if (targetUrl !== '/home' && targetUrl !== '/') {
|
|
||||||
this.router.navigate(['/home'], { replaceUrl: true });
|
// Gestione popstate (tasto indietro browser / gesture PWA)
|
||||||
}
|
this.router.events.pipe(
|
||||||
|
filter((e): e is NavigationStart => e instanceof NavigationStart),
|
||||||
|
filter(e => e.navigationTrigger === 'popstate')
|
||||||
|
).subscribe(() => {
|
||||||
|
const currentPath = this.router.url.split('?')[0];
|
||||||
|
if (currentPath === '/home' || currentPath === '/') {
|
||||||
|
// Spec 2: Sulla home, usciamo dall'app
|
||||||
|
this.router.navigate(['/home'], { replaceUrl: true });
|
||||||
|
this.exitApp();
|
||||||
|
} else if (currentPath === '/player' || currentPath === '/display') {
|
||||||
|
// Spec 1: Su un canto, andiamo alla home azzerando lo stack
|
||||||
|
this.navCtrl.navigateRoot('/home', { animationDirection: 'back' });
|
||||||
}
|
}
|
||||||
|
// Spec 3: Per il resto (es. /settings), popstate prosegue normalmente verso la pagina precedente
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestione tasto back hardware per nativo (Capacitor/Cordova)
|
// Gestione tasto indietro hardware (es. Android / Capacitor / PWA)
|
||||||
this.platform.backButton.subscribeWithPriority(9999, () => {
|
this.platform.backButton.subscribeWithPriority(10, async () => {
|
||||||
const currentUrl = this.router.url;
|
const currentPath = this.router.url.split('?')[0];
|
||||||
const path = currentUrl.split('?')[0];
|
if (currentPath === '/home' || currentPath === '/') {
|
||||||
if (path !== '/home' && path !== '/') {
|
// Spec 2: Sulla home, usciamo dall'app
|
||||||
this.router.navigate(['/home']);
|
this.exitApp();
|
||||||
|
} else if (currentPath === '/player' || currentPath === '/display') {
|
||||||
|
// Spec 1: Su un canto, andiamo alla home azzerando lo stack
|
||||||
|
this.navCtrl.navigateRoot('/home', { animationDirection: 'back' });
|
||||||
} else {
|
} else {
|
||||||
App.exitApp();
|
// Spec 3: Per il resto, logica standard (pagina precedente)
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
this.location.back();
|
||||||
|
} else {
|
||||||
|
this.navCtrl.navigateRoot('/home', { animationDirection: 'back' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,23 +332,70 @@ export class AppComponent implements OnInit {
|
|||||||
console.log('[AppComponent] PWA appinstalled event caught.');
|
console.log('[AppComponent] PWA appinstalled event caught.');
|
||||||
localStorage.setItem('pwa-installed', 'true');
|
localStorage.setItem('pwa-installed', 'true');
|
||||||
this.isPwaInstalled.set(true);
|
this.isPwaInstalled.set(true);
|
||||||
this.isInstalling.set(false);
|
|
||||||
this.isRedirecting.set(true);
|
|
||||||
this.showInstallOverlay.set(false);
|
this.showInstallOverlay.set(false);
|
||||||
this.showRedirectOverlay.set(false);
|
this.showRedirectOverlay.set(false);
|
||||||
|
|
||||||
|
// Se la finestra è GIÀ stata trasformata in PWA standalone (es. Desktop Mac/Windows)
|
||||||
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
|
||||||
|
if (isStandalone) {
|
||||||
|
this.isInstalling.set(false);
|
||||||
|
this.isRedirecting.set(false);
|
||||||
|
this.checkLoaderDismissal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Su Android, l'evento 'appinstalled' scatta in 2-3 secondi, MA l'OS impiega 10-12s
|
||||||
|
// per pacchettizzare e registrare il WebAPK nella schermata Home.
|
||||||
|
// Calibriamo la progress bar spalmata su ~11 secondi reali prima del reindirizzamento.
|
||||||
|
this.isInstalling.set(true);
|
||||||
|
this.isRedirecting.set(false);
|
||||||
|
|
||||||
if ((window as any).PwaLoader) {
|
if ((window as any).PwaLoader) {
|
||||||
(window as any).PwaLoader.show();
|
(window as any).PwaLoader.show();
|
||||||
(window as any).PwaLoader.update({
|
(window as any).PwaLoader.update({
|
||||||
title: 'Chiudi il browser',
|
title: 'Installazione applicazione',
|
||||||
desc: 'Applicazione installata con successo! Chiudi il browser e continua sulla PWA.',
|
phase: 'Fase: Registrazione',
|
||||||
isRedirect: true
|
desc: 'Generazione e registrazione dell\'applicazione sul dispositivo in corso...',
|
||||||
|
percent: 15
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
const startTime = Date.now();
|
||||||
window.location.href = this.protocolLink;
|
const TARGET_DURATION_MS = 11000; // 11 secondi reali per completare l'installazione WebAPK
|
||||||
}, 1000);
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
let pct = Math.min(100, Math.round(15 + (elapsed / TARGET_DURATION_MS) * 85));
|
||||||
|
|
||||||
|
if ((window as any).PwaLoader) {
|
||||||
|
(window as any).PwaLoader.update({ percent: pct });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pct >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
this.isInstalling.set(false);
|
||||||
|
this.isRedirecting.set(true);
|
||||||
|
|
||||||
|
if ((window as any).PwaLoader) {
|
||||||
|
(window as any).PwaLoader.update({
|
||||||
|
title: 'Chiudi il browser',
|
||||||
|
desc: 'Applicazione installata con successo! Chiudi il browser e continua sulla PWA.',
|
||||||
|
isRedirect: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.checkAndRedirectToPwa();
|
this.checkAndRedirectToPwa();
|
||||||
@@ -222,9 +443,8 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkVersionSync(): Promise<boolean> {
|
async checkVersionSync(): Promise<boolean> {
|
||||||
// Controlla SEMPRE version.json per primo — è il modo più affidabile per
|
// Verifica all'avvio se è disponibile una nuova versione remota (sia tramite version.json che SwUpdate)
|
||||||
// rilevare un disallineamento di versione, indipendentemente dallo stato del SW.
|
// Se disponibile, viene aggiornata e attivata automaticamente senza richiedere l'intervento dell'utente.
|
||||||
// Su mobile, checkForUpdate() può essere lento o inaffidabile.
|
|
||||||
try {
|
try {
|
||||||
console.log(`[PWA-Update] Verifica version.json all'avvio (locale=${VERSION})...`);
|
console.log(`[PWA-Update] Verifica version.json all'avvio (locale=${VERSION})...`);
|
||||||
const response = await Promise.race([
|
const response = await Promise.race([
|
||||||
@@ -234,10 +454,9 @@ export class AppComponent implements OnInit {
|
|||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data && data.version && data.version !== VERSION) {
|
if (data && data.version && data.version !== VERSION) {
|
||||||
console.log(`[PWA-Update] Mismatch rilevato: locale=${VERSION}, remota=${data.version}. Forza aggiornamento...`);
|
console.log(`[PWA-Update] Mismatch rilevato: locale=${VERSION}, remota=${data.version}. Avvio aggiornamento automatico...`);
|
||||||
const overlay = showFullscreenUpdateOverlay();
|
|
||||||
|
|
||||||
// Prova ad attivare tramite SwUpdate se abilitato (scarica il nuovo bundle SW)
|
// Scarica e attiva il nuovo Service Worker se abilitato
|
||||||
if (this.swUpdate.isEnabled) {
|
if (this.swUpdate.isEnabled) {
|
||||||
try {
|
try {
|
||||||
const hasSwUpdate = await Promise.race([
|
const hasSwUpdate = await Promise.race([
|
||||||
@@ -245,7 +464,7 @@ export class AppComponent implements OnInit {
|
|||||||
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 8000))
|
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 8000))
|
||||||
]);
|
]);
|
||||||
if (hasSwUpdate) {
|
if (hasSwUpdate) {
|
||||||
console.log('[PWA-Update] SW aggiornamento disponibile, attivazione...');
|
console.log('[PWA-Update] SW aggiornamento disponibile, attivazione automatica...');
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
this.swUpdate.activateUpdate(),
|
this.swUpdate.activateUpdate(),
|
||||||
new Promise<void>((resolve) => setTimeout(resolve, 5000))
|
new Promise<void>((resolve) => setTimeout(resolve, 5000))
|
||||||
@@ -256,7 +475,7 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggiorna anche la registrazione SW direttamente (doppia sicurezza)
|
// Forziamo il controllo di aggiornamento della registrazione Service Worker
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await navigator.serviceWorker.ready;
|
||||||
@@ -266,7 +485,7 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disattiva service worker attivi per forzare il refresh completo
|
// Deregistra i vecchi Service Worker per applicare la nuova versione pulita
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
for (const registration of registrations) {
|
for (const registration of registrations) {
|
||||||
@@ -274,7 +493,7 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancella le cache del browser
|
// Pulisci le cache del browser
|
||||||
if ('caches' in window) {
|
if ('caches' in window) {
|
||||||
const keys = await caches.keys();
|
const keys = await caches.keys();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
@@ -282,104 +501,34 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.finish();
|
// Ricarica la pagina in modo trasparente
|
||||||
|
|
||||||
// Ricarica con parametro cache-busting per forzare l'allineamento remoto
|
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set('update_cb', Date.now().toString());
|
url.searchParams.set('update_cb', Date.now().toString());
|
||||||
window.location.replace(url.toString());
|
window.location.replace(url.toString());
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.log('[PWA-Update] Versione allineata, nessun aggiornamento necessario.');
|
console.log('[PWA-Update] Versione allineata, nessun aggiornamento necessario.');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[PWA-Update] version.json check fallito:', e);
|
console.warn('[PWA-Update] version.json check fallito:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: Prova SwUpdate nel caso in cui il controllo version.json sia fallito o sia stato servito dalla cache
|
|
||||||
if (this.swUpdate.isEnabled) {
|
|
||||||
try {
|
|
||||||
console.log('[PWA-Update] Verifica aggiornamenti via SwUpdate all\'avvio...');
|
|
||||||
|
|
||||||
let activated = false;
|
|
||||||
let overlay: any = null;
|
|
||||||
|
|
||||||
const activateAndReload = async () => {
|
|
||||||
if (activated) return;
|
|
||||||
activated = true;
|
|
||||||
try {
|
|
||||||
await this.swUpdate.activateUpdate();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[PWA-Update] activateUpdate fallito all\'avvio:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deregistra i vecchi SW e cancella le cache per un ricaricamento pulito
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
||||||
for (const reg of registrations) {
|
|
||||||
await reg.unregister();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ('caches' in window) {
|
|
||||||
const keys = await caches.keys();
|
|
||||||
for (const key of keys) {
|
|
||||||
await caches.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlay) overlay.finish();
|
|
||||||
setTimeout(() => {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('update_cb', Date.now().toString());
|
|
||||||
window.location.replace(url.toString());
|
|
||||||
}, 600);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sottoscrivi PRIMA di verificare l'aggiornamento per evitare race condition
|
|
||||||
const sub = this.swUpdate.versionUpdates
|
|
||||||
.pipe(
|
|
||||||
filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
|
|
||||||
first()
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
console.log('[PWA-Update] VERSION_READY ricevuto all\'avvio');
|
|
||||||
activateAndReload();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Concedi fino a 8 secondi al controllo SW — le connessioni mobili possono essere lente
|
|
||||||
const hasUpdate = await Promise.race([
|
|
||||||
this.swUpdate.checkForUpdate(),
|
|
||||||
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 8000))
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (hasUpdate) {
|
|
||||||
console.log('[PWA-Update] Aggiornamento rilevato via SwUpdate. Avvio download...');
|
|
||||||
overlay = showFullscreenUpdateOverlay();
|
|
||||||
|
|
||||||
// Timeout di sicurezza di 25 secondi: se VERSION_READY non arriva, attiva comunque
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[PWA-Update] Safety timeout raggiunto all\'avvio, procedo...');
|
|
||||||
sub.unsubscribe();
|
|
||||||
activateAndReload();
|
|
||||||
}, 25000);
|
|
||||||
|
|
||||||
return true; // Attendi il ricaricamento
|
|
||||||
} else {
|
|
||||||
sub.unsubscribe();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[PWA-Update] Controllo SwUpdate fallito all\'avvio:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkAndRedirectToPwa() {
|
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;
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
|
||||||
if (isStandalone) {
|
if (isStandalone) {
|
||||||
localStorage.setItem('pwa-installed', 'true');
|
localStorage.setItem('pwa-installed', 'true');
|
||||||
|
this.checkLoaderDismissal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,34 +536,23 @@ export class AppComponent implements OnInit {
|
|||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
this.protocolLink = `web+canti://open${path}${search}`;
|
this.protocolLink = `web+canti://open${path}${search}`;
|
||||||
|
|
||||||
// Controlliamo se abbiamo già salvato che l'app è installata o se possiamo verificarlo
|
let isInstalled = false;
|
||||||
let isInstalled = localStorage.getItem('pwa-installed') === 'true';
|
if ('getInstalledRelatedApps' in navigator) {
|
||||||
if (!isInstalled && 'getInstalledRelatedApps' in navigator) {
|
|
||||||
try {
|
try {
|
||||||
const relatedApps = await (navigator as any).getInstalledRelatedApps();
|
const relatedApps = await (navigator as any).getInstalledRelatedApps();
|
||||||
isInstalled = relatedApps.length > 0;
|
isInstalled = relatedApps.length > 0;
|
||||||
if (isInstalled) {
|
|
||||||
localStorage.setItem('pwa-installed', 'true');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to check installed apps:', e);
|
console.warn('Failed to check installed apps:', e);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
isInstalled = localStorage.getItem('pwa-installed') === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se non è rilevata in localStorage/relatedApps ed è Android o Desktop con supporto ai prompt:
|
// Se prima era salvata come installata ma l'utente riceve il prima possibile un beforeinstallprompt,
|
||||||
// attendiamo 1.5s per dare tempo all'evento 'beforeinstallprompt' di scattare.
|
// significa che l'app è stata disinstallata!
|
||||||
// Se non scatta, significa che l'app è già installata.
|
if (this.settingsService.deferredPrompt() || this.settingsService.showInstallButton()) {
|
||||||
if (!isInstalled && !this.settingsService.isIos() && ('onbeforeinstallprompt' in window)) {
|
isInstalled = false;
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
localStorage.setItem('pwa-installed', 'false');
|
||||||
isInstalled = localStorage.getItem('pwa-installed') === 'true';
|
|
||||||
if (!isInstalled) {
|
|
||||||
const hasPrompt = this.settingsService.showInstallButton() || this.settingsService.deferredPrompt();
|
|
||||||
if (!hasPrompt) {
|
|
||||||
console.log('[AppComponent] PWA detected as already installed (onbeforeinstallprompt supported but no prompt fired).');
|
|
||||||
isInstalled = true;
|
|
||||||
localStorage.setItem('pwa-installed', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isPwaInstalled.set(isInstalled);
|
this.isPwaInstalled.set(isInstalled);
|
||||||
@@ -553,6 +691,20 @@ export class AppComponent implements OnInit {
|
|||||||
this.checkLoaderDismissal();
|
this.checkLoaderDismissal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private exitApp() {
|
||||||
|
try {
|
||||||
|
App.exitApp();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
if ((navigator as any)?.app?.exitApp) {
|
||||||
|
(navigator as any).app.exitApp();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
window.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showFullscreenUpdateOverlay() {
|
export function showFullscreenUpdateOverlay() {
|
||||||
@@ -601,3 +753,4 @@ export function showFullscreenUpdateOverlay() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,12 @@
|
|||||||
<zxing-scanner
|
<zxing-scanner
|
||||||
[formats]="allowedFormats"
|
[formats]="allowedFormats"
|
||||||
[device]="currentDevice"
|
[device]="currentDevice"
|
||||||
|
[autostart]="true"
|
||||||
|
[tryHarder]="false"
|
||||||
(camerasFound)="onCamerasFound($event)"
|
(camerasFound)="onCamerasFound($event)"
|
||||||
(scanSuccess)="onCodeResult($event)">
|
(scanSuccess)="onCodeResult($event)"
|
||||||
|
(scanError)="onScanError($event)"
|
||||||
|
(scanFailure)="onScanFailure($event)">
|
||||||
</zxing-scanner>
|
</zxing-scanner>
|
||||||
|
|
||||||
<div class="scan-overlay">
|
<div class="scan-overlay">
|
||||||
|
|||||||
@@ -55,6 +55,21 @@ export class QrScannerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onScanError(error: any) {
|
||||||
|
// Silenzia e ignora gli errori interni della libreria zxing
|
||||||
|
try {
|
||||||
|
if (error) {
|
||||||
|
// Previeni la propagazione a livello di window/console
|
||||||
|
if (typeof error.preventDefault === 'function') error.preventDefault();
|
||||||
|
if (typeof error.stopPropagation === 'function') error.stopPropagation();
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onScanFailure(failure: any) {
|
||||||
|
// Ignora i tentativi falliti di decodifica frame per frame
|
||||||
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.modalCtrl.dismiss();
|
this.modalCtrl.dismiss();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<ion-header class="ion-no-border">
|
||||||
|
<ion-toolbar class="bg-gradient">
|
||||||
|
<ion-title class="outfit-font">Condividi Playlist</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button (click)="dismiss()">
|
||||||
|
<ion-icon name="close-outline" slot="icon-only" color="secondary"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="bg-gradient ion-padding">
|
||||||
|
<div class="qr-container">
|
||||||
|
<p class="description outfit-font">
|
||||||
|
Fai scansionare questo QR Code da un altro dispositivo per condividere all'istante la playlist <strong>{{ playlistName }}</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="qr-card glass">
|
||||||
|
<div class="qr-wrapper" *ngIf="qrCodeUrl">
|
||||||
|
<img [src]="qrCodeUrl" alt="QR Code della Playlist" class="qr-image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playlist-box">
|
||||||
|
<span class="playlist-label outfit-font">Nome Playlist:</span>
|
||||||
|
<span class="playlist-text outfit-font">{{ playlistName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-wrapper">
|
||||||
|
<ion-button expand="block" fill="solid" color="secondary" class="outfit-font action-btn" (click)="shareLinkNative()">
|
||||||
|
<ion-icon name="share-social-outline" slot="start"></ion-icon>
|
||||||
|
Invia Link / Condividi
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-button expand="block" fill="outline" color="secondary" class="outfit-font action-btn" (click)="copyToClipboard()">
|
||||||
|
<ion-icon name="copy-outline" slot="start"></ion-icon>
|
||||||
|
Copia Link
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
.qr-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 8px;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--ion-text-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-card {
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrapper {
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--ion-text-color);
|
||||||
|
font-weight: bold;
|
||||||
|
word-break: break-word;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
margin: 0;
|
||||||
|
--border-radius: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Component, OnInit, inject, Input } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule, ModalController, ToastController } from '@ionic/angular';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-share-playlist-qr-modal',
|
||||||
|
templateUrl: './share-playlist-qr-modal.component.html',
|
||||||
|
styleUrls: ['./share-playlist-qr-modal.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, IonicModule]
|
||||||
|
})
|
||||||
|
export class SharePlaylistQrModalComponent implements OnInit {
|
||||||
|
private modalCtrl = inject(ModalController);
|
||||||
|
private toastCtrl = inject(ToastController);
|
||||||
|
|
||||||
|
@Input() playlistName!: string;
|
||||||
|
@Input() shareLink!: string;
|
||||||
|
|
||||||
|
public qrCodeUrl: string = '';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.generateQr();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateQr() {
|
||||||
|
try {
|
||||||
|
this.qrCodeUrl = await QRCode.toDataURL(this.shareLink, {
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
margin: 2,
|
||||||
|
width: 400,
|
||||||
|
color: {
|
||||||
|
dark: '#1e293b',
|
||||||
|
light: '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to generate QR Code:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async shareLinkNative() {
|
||||||
|
const fileName = `${this.playlistName.toLowerCase().replace(/\s+/g, '_')}_qr.png`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(this.qrCodeUrl);
|
||||||
|
const blob = await res.blob();
|
||||||
|
const file = new File([blob], fileName, { type: 'image/png' });
|
||||||
|
|
||||||
|
const isMac = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent) ||
|
||||||
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||||
|
|
||||||
|
if (!isMac && navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||||
|
await navigator.share({
|
||||||
|
files: [file],
|
||||||
|
title: 'Playlist CantiCristiani',
|
||||||
|
text: `Ecco la playlist: ${this.playlistName}\n\nClicca qui per aprirla subito: ${this.shareLink}`
|
||||||
|
});
|
||||||
|
} else if (navigator.share) {
|
||||||
|
await navigator.share({
|
||||||
|
title: 'Playlist CantiCristiani',
|
||||||
|
text: `Ecco la playlist: ${this.playlistName}\n\nClicca qui per aprirla: ${this.shareLink}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.copyToClipboard();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Share failed', err);
|
||||||
|
await this.copyToClipboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyToClipboard() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.shareLink);
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: 'Link copiato negli appunti!',
|
||||||
|
duration: 2000,
|
||||||
|
color: 'success',
|
||||||
|
position: 'bottom'
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy text:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.modalCtrl.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
+329
-153
@@ -5,20 +5,26 @@
|
|||||||
<img src="assets/icon/favicon.png" class="header-logo">
|
<img src="assets/icon/favicon.png" class="header-logo">
|
||||||
<div class="header-text-group">
|
<div class="header-text-group">
|
||||||
<span class="app-name">{{ appName }}</span>
|
<span class="app-name">{{ appName }}</span>
|
||||||
<span class="version-badge" (click)="checkForAppUpdate($event)" style="cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">
|
<span class="version-badge" style="cursor: default; display: inline-flex; align-items: center; gap: 4px;">
|
||||||
v{{ version }}<span *ngIf="settingsService.userName()"> - {{ settingsService.userName() }}</span>
|
v{{ version }}
|
||||||
<span *ngIf="hasUpdateAvailable()" style="display: inline-flex; align-items: center; justify-content: center; padding: 4px; margin-left: 2px; background: rgba(var(--ion-color-secondary-rgb), 0.25); border-radius: 50%; border: 1px solid var(--ion-color-secondary); width: 24px; height: 24px; box-shadow: 0 0 8px rgba(var(--ion-color-secondary-rgb), 0.3);">
|
|
||||||
<ion-icon name="refresh-outline" style="font-size: 1.15rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ion-title>
|
</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
|
<ion-button routerLink="/propose-canto" class="add-btn" title="Crea un nuovo canto" *ngIf="settingsService.showEditor()">
|
||||||
<ion-button (click)="importPlaylist()" class="add-btn">
|
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
|
||||||
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
|
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<ng-container *ngIf="hasUpdateAvailable(); else showQrBtn">
|
||||||
|
<ion-button (click)="checkForAppUpdate($event)" class="add-btn" title="Aggiornamento disponibile">
|
||||||
|
<ion-icon slot="icon-only" name="refresh-outline" color="secondary"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #showQrBtn>
|
||||||
|
<ion-button (click)="importPlaylist()" class="add-btn">
|
||||||
|
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ng-template>
|
||||||
<ion-button routerLink="/settings" class="settings-btn">
|
<ion-button routerLink="/settings" class="settings-btn">
|
||||||
<ion-icon slot="icon-only" name="settings-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="settings-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -32,110 +38,322 @@
|
|||||||
placeholder="Cerca un canto..."
|
placeholder="Cerca un canto..."
|
||||||
[value]="searchQuery()"
|
[value]="searchQuery()"
|
||||||
(ionInput)="onSearch($event)"
|
(ionInput)="onSearch($event)"
|
||||||
|
[disabled]="isAdvancedSearchOpen()"
|
||||||
class="custom-searchbar">
|
class="custom-searchbar">
|
||||||
</ion-searchbar>
|
</ion-searchbar>
|
||||||
<ion-button fill="clear" (click)="toggleVoiceSearch()" class="voice-search-btn">
|
<ion-button fill="clear" (click)="toggleAdvancedSearch()" class="adv-search-btn" [class.active]="isAdvancedSearchOpen()" title="Ricerca avanzata">
|
||||||
<ion-icon slot="icon-only" [name]="audioEngine.isSearching() ? 'mic' : 'mic-outline'" [color]="audioEngine.isSearching() ? 'danger' : 'secondary'"></ion-icon>
|
<ion-icon slot="icon-only" name="options-outline" [color]="isAdvancedSearchOpen() ? 'secondary' : 'medium'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" (click)="toggleVoiceSearch('global')" class="voice-search-btn" [disabled]="isAdvancedSearchOpen()">
|
||||||
|
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'global') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'global') ? 'danger' : 'secondary'"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-actions-row">
|
<!-- Advanced Search & Filters Toggle Row -->
|
||||||
<div class="song-count-card outfit-font">
|
<div class="advanced-search-toggle-row">
|
||||||
{{ filteredCanti().length }}
|
<div class="toggle-row-left">
|
||||||
</div>
|
<div class="song-count-card outfit-font">
|
||||||
<div class="filter-buttons">
|
{{ filteredCanti().length }}
|
||||||
<ion-button
|
</div>
|
||||||
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
|
<button type="button" class="advanced-search-link outfit-font" (click)="toggleAdvancedSearch()">
|
||||||
size="small"
|
<ion-icon [name]="isAdvancedSearchOpen() ? 'chevron-up-outline' : 'options-outline'"></ion-icon>
|
||||||
(click)="toggleFilterType('playlist')"
|
<span>Ricerche</span>
|
||||||
class="filter-chip">
|
<span class="inline-clear-btn" *ngIf="hasAdvancedSearchParams()" (click)="clearAdvancedSearch($event)" title="Reset ricerche">
|
||||||
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
<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>
|
|
||||||
</span>
|
</span>
|
||||||
</ion-button>
|
</button>
|
||||||
|
<button type="button" class="advanced-search-link outfit-font" (click)="toggleFilterCard()">
|
||||||
|
<ion-icon [name]="isFilterCardOpen() ? 'chevron-up-outline' : 'filter-outline'"></ion-icon>
|
||||||
|
<span>Filtri</span>
|
||||||
|
<span class="inline-clear-btn" *ngIf="hasFilterCardParams()" (click)="clearFilterCard($event)" title="Reset filtri">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="advanced-search-link outfit-font" (click)="togglePlaylistCard()">
|
||||||
|
<ion-icon [name]="isPlaylistCardOpen() ? 'chevron-up-outline' : 'list-outline'"></ion-icon>
|
||||||
|
<span>Liste</span>
|
||||||
|
<span class="inline-clear-btn" *ngIf="hasPlaylistCardParams()" (click)="clearPlaylistCard($event)" title="Reset liste">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="comunita-filter-wrapper" style="position: relative; display: inline-flex; align-items: center; margin-right: 8px; z-index: 999;" *ngIf="settingsService.comunitaEnabled()">
|
<!-- Advanced Search Panel -->
|
||||||
<ion-button
|
<div class="advanced-search-panel glass outfit-font"
|
||||||
[fill]="comunitaService.isFilterActive() ? 'solid' : 'outline'"
|
*ngIf="isAdvancedSearchOpen()"
|
||||||
size="small"
|
(touchstart)="onPanelTouchStart($event)"
|
||||||
(click)="toggleComunitaFilter()"
|
(touchend)="onPanelTouchEnd($event, 'advancedSearch')">
|
||||||
[color]="comunitaService.isFilterActive() ? 'secondary' : 'medium'"
|
<div class="adv-search-grid">
|
||||||
class="filter-chip"
|
<div class="adv-input-group">
|
||||||
style="margin: 0;">
|
<label class="adv-label">Ricerca per titolo</label>
|
||||||
<ion-icon slot="start" name="people-outline" style="font-size: 1.1rem; margin-right: 4px;"></ion-icon>
|
<div class="adv-input-wrapper">
|
||||||
{{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }}
|
<ion-icon name="text-outline" class="adv-input-icon"></ion-icon>
|
||||||
</ion-button>
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Titolo canto..."
|
||||||
|
[value]="searchTitle()"
|
||||||
|
(input)="onSearchTitleInput($event)"
|
||||||
|
class="adv-input" />
|
||||||
|
<ion-icon name="close-circle" *ngIf="searchTitle()" (click)="searchTitle.set('')" class="adv-clear-icon"></ion-icon>
|
||||||
|
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('title')" class="adv-mic-btn">
|
||||||
|
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'title') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'title') ? 'danger' : 'secondary'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<div class="adv-input-group">
|
||||||
[fill]="activeFilterType() === 'lista_completa' || showValidati() || showNonValidati() ? 'solid' : 'outline'"
|
<label class="adv-label">Ricerca per autore</label>
|
||||||
size="small"
|
<div class="adv-input-wrapper">
|
||||||
(click)="toggleFilterType('lista_completa')"
|
<ion-icon name="person-outline" class="adv-input-icon"></ion-icon>
|
||||||
class="filter-chip">
|
<input
|
||||||
{{ showValidati() ? 'Lista: Validati' : (showNonValidati() ? 'Lista: Non Validati' : 'Lista completa') }}
|
type="text"
|
||||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="!showValidati() && !showNonValidati()"></ion-icon>
|
placeholder="Nome autore..."
|
||||||
<span class="close-icon-wrapper" *ngIf="showValidati() || showNonValidati()" (click)="clearListaCompleta($event)">
|
[value]="searchAuthor()"
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
(input)="onSearchAuthorInput($event)"
|
||||||
</span>
|
class="adv-input" />
|
||||||
</ion-button>
|
<ion-icon name="close-circle" *ngIf="searchAuthor()" (click)="searchAuthor.set('')" class="adv-clear-icon"></ion-icon>
|
||||||
|
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('author')" class="adv-mic-btn">
|
||||||
|
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'author') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'author') ? 'danger' : 'secondary'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<div class="adv-input-group">
|
||||||
[fill]="activeFilterType() === 'liturgico' || selectedLiturgico() !== null ? 'solid' : 'outline'"
|
<label class="adv-label">Ricerca per testo</label>
|
||||||
size="small"
|
<div class="adv-input-wrapper">
|
||||||
(click)="toggleFilterType('liturgico')"
|
<ion-icon name="document-text-outline" class="adv-input-icon"></ion-icon>
|
||||||
class="filter-chip">
|
<input
|
||||||
{{ selectedLiturgico() !== null ? 'Liturgia: ' + getSelectedLiturgicoLabel() : 'Liturgia' }}
|
type="text"
|
||||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="selectedLiturgico() === null"></ion-icon>
|
placeholder="Parole nel testo..."
|
||||||
<span class="close-icon-wrapper" *ngIf="selectedLiturgico() !== null" (click)="clearLiturgico($event)">
|
[value]="searchText()"
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
(input)="onSearchTextInput($event)"
|
||||||
</span>
|
class="adv-input" />
|
||||||
</ion-button>
|
<ion-icon name="close-circle" *ngIf="searchText()" (click)="searchText.set('')" class="adv-clear-icon"></ion-icon>
|
||||||
|
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('text')" class="adv-mic-btn">
|
||||||
|
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'text') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'text') ? 'danger' : 'secondary'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<div class="adv-input-group">
|
||||||
[fill]="activeFilterType() === 'tematico' || selectedTematico() !== null ? 'solid' : 'outline'"
|
<label class="adv-label">Ricerca per nr canto</label>
|
||||||
size="small"
|
<div class="adv-input-wrapper">
|
||||||
(click)="toggleFilterType('tematico')"
|
<ion-icon name="pricetag-outline" class="adv-input-icon"></ion-icon>
|
||||||
class="filter-chip">
|
<input
|
||||||
{{ selectedTematico() !== null ? 'Periodo: ' + getSelectedTematicoLabel() : 'Periodo' }}
|
type="text"
|
||||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="selectedTematico() === null"></ion-icon>
|
placeholder="Numero canto..."
|
||||||
<span class="close-icon-wrapper" *ngIf="selectedTematico() !== null" (click)="clearTematico($event)">
|
[value]="searchNumber()"
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
(input)="onSearchNumberInput($event)"
|
||||||
</span>
|
class="adv-input" />
|
||||||
</ion-button>
|
<ion-icon name="close-circle" *ngIf="searchNumber()" (click)="searchNumber.set('')" class="adv-clear-icon"></ion-icon>
|
||||||
|
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('number')" class="adv-mic-btn">
|
||||||
|
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'number') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'number') ? 'danger' : 'secondary'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<!-- Playlist Card (default closed) -->
|
||||||
[fill]="showSuggeriti() ? 'solid' : 'outline'"
|
<div class="filter-card glass outfit-font"
|
||||||
size="small"
|
*ngIf="isPlaylistCardOpen()"
|
||||||
(click)="toggleSuggeriti()"
|
(touchstart)="onPanelTouchStart($event)"
|
||||||
class="filter-chip">
|
(touchend)="onPanelTouchEnd($event, 'playlistCard')">
|
||||||
Suggeriti
|
<div class="filter-card-row">
|
||||||
<span class="close-icon-wrapper" *ngIf="showSuggeriti()" (click)="clearSuggeriti($event)">
|
<span class="filter-row-label">Playlist</span>
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<div class="filter-row-items">
|
||||||
</span>
|
<!-- Comunità filter button if enabled -->
|
||||||
</ion-button>
|
<div class="comunita-filter-wrapper" style="position: relative; display: inline-flex; align-items: center; z-index: 999;" *ngIf="settingsService.comunitaEnabled()">
|
||||||
|
<ion-button
|
||||||
|
[fill]="comunitaService.isFilterActive() ? 'solid' : 'outline'"
|
||||||
|
size="small"
|
||||||
|
(click)="toggleComunitaFilter()"
|
||||||
|
[color]="comunitaService.isFilterActive() ? 'secondary' : 'medium'"
|
||||||
|
class="filter-chip"
|
||||||
|
style="margin: 0;">
|
||||||
|
<ion-icon slot="start" name="people-outline" style="font-size: 1.1rem; margin-right: 4px;"></ion-icon>
|
||||||
|
{{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<!-- Playlist Chips -->
|
||||||
[fill]="showTopTen() ? 'solid' : 'outline'"
|
<div
|
||||||
size="small"
|
*ngFor="let item of playlistService.allPlaylists()"
|
||||||
(click)="toggleTopTen()"
|
class="momento-chip glass"
|
||||||
class="filter-chip">
|
[class.active]="playlistService.activePlaylistId() === item.id"
|
||||||
Top Ten
|
[class.comunita-chip]="item.isComunita"
|
||||||
<span class="close-icon-wrapper" *ngIf="showTopTen()" (click)="clearTopTen($event)">
|
(click)="playlistService.activePlaylistId() === item.id ? clearSpecialList($event) : selectPlaylist(item)">
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon *ngIf="item.isComunita" name="people-outline" style="font-size: 0.85rem; margin-right: 4px; vertical-align: middle;"></ion-icon>
|
||||||
|
{{ item.name }}
|
||||||
|
<span *ngIf="playlistService.activePlaylistId() === item.id && 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: 18px; height: 18px; vertical-align: middle;">
|
||||||
|
<ion-icon name="refresh-outline" style="font-size: 0.85rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
|
||||||
|
</span>
|
||||||
|
<span class="close-icon-wrapper" *ngIf="playlistService.activePlaylistId() === item.id" (click)="clearSpecialList($event)">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span *ngIf="playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()" class="empty-playlist-text">
|
||||||
|
Nessuna playlist creata
|
||||||
</span>
|
</span>
|
||||||
</ion-button>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Card (default closed) -->
|
||||||
|
<div class="filter-card glass outfit-font"
|
||||||
|
*ngIf="isFilterCardOpen()"
|
||||||
|
(touchstart)="onPanelTouchStart($event)"
|
||||||
|
(touchend)="onPanelTouchEnd($event, 'filterCard')">
|
||||||
|
|
||||||
|
<!-- Row 2: Tipologia -->
|
||||||
|
<div class="filter-card-row">
|
||||||
|
<span class="filter-row-label">Tipologia</span>
|
||||||
|
<div class="filter-row-items">
|
||||||
|
<div
|
||||||
|
class="momento-chip glass"
|
||||||
|
[class.active]="showValidati()"
|
||||||
|
(click)="toggleValidati()">
|
||||||
|
Validati
|
||||||
|
<span class="close-icon-wrapper" *ngIf="showValidati()" (click)="clearListaCompleta($event)">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="momento-chip glass"
|
||||||
|
[class.active]="showNonValidati()"
|
||||||
|
(click)="toggleNonValidati()"
|
||||||
|
style="display: inline-flex; align-items: center; gap: 6px;">
|
||||||
|
<span>Non Validati</span>
|
||||||
|
<ion-icon
|
||||||
|
*ngIf="showNonValidati() && !playlistService.selectionMode() && settingsService.showEditor()"
|
||||||
|
name="add-outline"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
routerLink="/propose-canto"
|
||||||
|
class="non-validati-add-icon">
|
||||||
|
</ion-icon>
|
||||||
|
<span class="close-icon-wrapper" *ngIf="showNonValidati()" (click)="clearListaCompleta($event)">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Liturgia -->
|
||||||
|
<div class="filter-card-row">
|
||||||
|
<span class="filter-row-label">Liturgia</span>
|
||||||
|
<div class="filter-row-items">
|
||||||
|
<div
|
||||||
|
*ngFor="let item of cantiService.indiceLiturgico()"
|
||||||
|
class="momento-chip glass"
|
||||||
|
[class.active]="selectedLiturgico() === item.id"
|
||||||
|
(click)="toggleIndex(item.id, 'liturgico')">
|
||||||
|
{{ item.tag_name }}
|
||||||
|
<span class="close-icon-wrapper" *ngIf="selectedLiturgico() === item.id" (click)="clearLiturgico($event)">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 4: Periodo -->
|
||||||
|
<div class="filter-card-row">
|
||||||
|
<span class="filter-row-label">Periodo</span>
|
||||||
|
<div class="filter-row-items">
|
||||||
|
<div
|
||||||
|
*ngFor="let item of cantiService.indiceTematico()"
|
||||||
|
class="momento-chip glass"
|
||||||
|
[class.active]="selectedTematico() === item.id"
|
||||||
|
(click)="toggleIndex(item.id, 'tematico')">
|
||||||
|
{{ item.tag_name }}
|
||||||
|
<span class="close-icon-wrapper" *ngIf="selectedTematico() === item.id" (click)="clearTematico($event)">
|
||||||
|
<ion-icon name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 5 (Footer): Suggeriti, Top Ten & Reset filtri -->
|
||||||
|
<div class="filter-card-footer">
|
||||||
|
<div class="footer-actions">
|
||||||
|
<ion-button
|
||||||
|
[fill]="showSuggeriti() ? 'solid' : 'outline'"
|
||||||
|
size="small"
|
||||||
|
(click)="toggleSuggeriti()"
|
||||||
|
class="filter-chip">
|
||||||
|
Suggeriti
|
||||||
|
<span class="close-icon-wrapper" *ngIf="showSuggeriti()" (click)="clearSuggeriti($event)">
|
||||||
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-button
|
||||||
|
[fill]="showTopTen() ? 'solid' : 'outline'"
|
||||||
|
size="small"
|
||||||
|
(click)="toggleTopTen()"
|
||||||
|
class="filter-chip">
|
||||||
|
Top Ten
|
||||||
|
<span class="close-icon-wrapper" *ngIf="showTopTen()" (click)="clearTopTen($event)">
|
||||||
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-button
|
||||||
|
[fill]="showOnlyMine() ? 'solid' : 'outline'"
|
||||||
|
size="small"
|
||||||
|
(click)="toggleOnlyMine()"
|
||||||
|
class="filter-chip">
|
||||||
|
Miei
|
||||||
|
<span class="close-icon-wrapper" *ngIf="showOnlyMine()" (click)="clearOnlyMine($event)">
|
||||||
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|
||||||
|
<!-- Fixed Actions Bar in header for active playlist or selection mode -->
|
||||||
|
<ion-toolbar class="bg-gradient playlist-toolbar" *ngIf="(playlistService.activeListName() !== null && !playlistService.selectionMode()) || (playlistService.selectionMode() && reorderList().length > 0)">
|
||||||
|
<div class="playlist-actions-bar">
|
||||||
|
<!-- Duration Badge on the left -->
|
||||||
|
<div class="playlist-duration-pill outfit-font" *ngIf="totalPlaylistDuration()">
|
||||||
|
<ion-icon name="time-outline"></ion-icon>
|
||||||
|
<span>{{ totalPlaylistDuration() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Playlist Name in middle -->
|
||||||
|
<div class="playlist-title-badge outfit-font" *ngIf="playlistService.activeListName() && !playlistService.selectionMode()">
|
||||||
|
<span class="playlist-title-text">{{ playlistService.activeListName() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Playlist Actions -->
|
||||||
|
<div class="selection-pill glass" *ngIf="playlistService.activeListName() !== null && !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>
|
||||||
|
<ion-button fill="clear" color="secondary" (click)="cloneActivePlaylist()" class="mini-action-btn" title="Clona Playlist">
|
||||||
|
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" color="secondary" (click)="printActivePlaylist()" class="mini-action-btn" title="Esporta PDF">
|
||||||
|
<ion-icon slot="icon-only" name="print-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" color="secondary" (click)="shareActivePlaylist()" class="mini-action-btn">
|
||||||
|
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" color="danger" (click)="deleteActivePlaylist()" class="mini-action-btn" *ngIf="playlistService.activeListName() !== null && !isComunitaPlaylist()">
|
||||||
|
<ion-icon name="trash-outline" slot="icon-only"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Category Scroll (Dropdown style) -->
|
|
||||||
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="shouldShowSubSectionToolbar()">
|
|
||||||
<div class="momento-scroll" *ngIf="activeFilterType() !== 'playlist' || playlistService.allPlaylists().length > 0 || playlistService.selectionMode()">
|
|
||||||
<!-- Selection Mode Actions -->
|
<!-- Selection Mode Actions -->
|
||||||
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
|
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
|
||||||
<span class="selection-count">{{ reorderList().length }}</span>
|
<span class="selection-count">{{ reorderList().length }}</span>
|
||||||
@@ -156,65 +374,6 @@
|
|||||||
<ion-icon slot="icon-only" name="close-circle-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="close-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Active Playlist Actions -->
|
|
||||||
<div class="selection-pill glass" *ngIf="activeFilterType() === 'playlist' && playlistService.activePlaylistId() && !playlistService.selectionMode()">
|
|
||||||
<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>
|
|
||||||
<ion-button fill="clear" color="secondary" (click)="cloneActivePlaylist()" class="mini-action-btn" title="Clona Playlist">
|
|
||||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" color="secondary" (click)="shareActivePlaylist()" class="mini-action-btn">
|
|
||||||
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" color="danger" (click)="deleteActivePlaylist()" class="mini-action-btn" *ngIf="playlistService.activePlaylistId() && !isComunitaPlaylist()">
|
|
||||||
<ion-icon name="trash-outline" slot="icon-only"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filter chips -->
|
|
||||||
<ng-container *ngIf="activeFilterType() === 'lista_completa'">
|
|
||||||
<div
|
|
||||||
class="momento-chip glass"
|
|
||||||
[class.active]="showValidati()"
|
|
||||||
(click)="toggleValidati()">
|
|
||||||
Validati
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="momento-chip glass"
|
|
||||||
[class.active]="showNonValidati()"
|
|
||||||
(click)="toggleNonValidati()"
|
|
||||||
style="display: inline-flex; align-items: center; gap: 6px;">
|
|
||||||
<span>Non Validati</span>
|
|
||||||
<ion-icon
|
|
||||||
*ngIf="showNonValidati() && !playlistService.selectionMode() && settingsService.showEditor()"
|
|
||||||
name="add-outline"
|
|
||||||
(click)="$event.stopPropagation()"
|
|
||||||
routerLink="/propose-canto"
|
|
||||||
class="non-validati-add-icon">
|
|
||||||
</ion-icon>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="activeFilterType() !== 'lista_completa'">
|
|
||||||
<div
|
|
||||||
*ngFor="let item of (activeFilterType() === 'playlist' ? playlistService.allPlaylists() : (activeFilterType() === 'liturgico' ? cantiService.indiceLiturgico() : (activeFilterType() === 'tematico' ? cantiService.indiceTematico() : [])))"
|
|
||||||
class="momento-chip glass"
|
|
||||||
[class.active]="activeFilterType() === 'playlist' ? playlistService.activePlaylistId() === item.id : isIndexSelected(item.id)"
|
|
||||||
[class.comunita-chip]="activeFilterType() === 'playlist' && item.isComunita"
|
|
||||||
(click)="activeFilterType() === 'playlist' ? selectPlaylist(item) : toggleIndex(item.id, activeFilterType()!)">
|
|
||||||
<ion-icon *ngIf="activeFilterType() === 'playlist' && item.isComunita" name="people-outline" style="font-size: 0.85rem; margin-right: 4px; vertical-align: middle;"></ion-icon>
|
|
||||||
{{ activeFilterType() === 'playlist' ? item.name : item.tag_name }}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Instructions when playlist is empty -->
|
|
||||||
<div class="empty-playlist-container" *ngIf="activeFilterType() === 'playlist' && playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()">
|
|
||||||
<p class="empty-playlist-instruction outfit-font">
|
|
||||||
Per creare una playlist, seleziona il nr del canto che vuoi inserire nella playlist, riordinali e salvala con nome
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@@ -231,6 +390,7 @@
|
|||||||
<div class="loading-wrapper">
|
<div class="loading-wrapper">
|
||||||
<ion-spinner name="crescent" color="secondary"></ion-spinner>
|
<ion-spinner name="crescent" color="secondary"></ion-spinner>
|
||||||
<div class="percentage-label outfit-font">{{ cantiService.progress() }}%</div>
|
<div class="percentage-label outfit-font">{{ cantiService.progress() }}%</div>
|
||||||
|
<ion-progress-bar [value]="cantiService.progress() / 100" color="secondary" style="width: 200px; border-radius: 10px; height: 8px; margin-top: 8px;"></ion-progress-bar>
|
||||||
<p class="ion-margin-top" style="color: var(--ion-color-secondary)">Caricamento canti...</p>
|
<p class="ion-margin-top" style="color: var(--ion-color-secondary)">Caricamento canti...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,6 +471,23 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||||
<span>{{ canto.autore || 'Autore sconosciuto' }}</span>
|
<span>{{ canto.autore || 'Autore sconosciuto' }}</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px;"
|
||||||
|
*ngIf="(settingsService.showDurationBpmTonality() && (canto.durata || canto.bpm || getSongTonality(canto))) || (settingsService.showUpdateDate() && canto.data_update) || (showTopTen() && getEsecuzioniCount(canto.id) !== null) || (showSuggeriti() && getMassSuggestionWeight(canto.id_canti) !== null)">
|
||||||
|
<ng-container *ngIf="settingsService.showDurationBpmTonality()">
|
||||||
|
<span *ngIf="canto.durata" style="font-size: 0.7rem; color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); padding: 1px 5px; border-radius: 4px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15);">
|
||||||
|
<ion-icon name="time-outline" style="font-size: 0.75rem;"></ion-icon>
|
||||||
|
{{ canto.durata }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="canto.bpm" style="font-size: 0.7rem; color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); padding: 1px 5px; border-radius: 4px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15);">
|
||||||
|
<ion-icon name="pulse-outline" style="font-size: 0.75rem;"></ion-icon>
|
||||||
|
{{ canto.bpm }} BPM
|
||||||
|
</span>
|
||||||
|
<span *ngIf="getSongTonality(canto)" style="font-size: 0.7rem; color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); padding: 1px 5px; border-radius: 4px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15);">
|
||||||
|
<ion-icon name="musical-note-outline" style="font-size: 0.75rem;"></ion-icon>
|
||||||
|
{{ getSongTonality(canto) }}
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
<span *ngIf="settingsService.showUpdateDate() && canto.data_update" class="update-date-badge" style="font-size: 0.7rem; color: rgba(255, 255, 255, 0.45); font-weight: 400; display: inline-flex; align-items: center; gap: 3px;">
|
<span *ngIf="settingsService.showUpdateDate() && canto.data_update" class="update-date-badge" style="font-size: 0.7rem; color: rgba(255, 255, 255, 0.45); font-weight: 400; display: inline-flex; align-items: center; gap: 3px;">
|
||||||
<ion-icon name="calendar-outline" style="font-size: 0.75rem; color: rgba(255,255,255,0.45);"></ion-icon>
|
<ion-icon name="calendar-outline" style="font-size: 0.75rem; color: rgba(255,255,255,0.45);"></ion-icon>
|
||||||
agg. {{ cantiService.formatUpdateDate(canto.data_update) }}
|
agg. {{ cantiService.formatUpdateDate(canto.data_update) }}
|
||||||
@@ -362,8 +539,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Button (only for local songs) -->
|
<!-- Delete Button (only for "mio" songs when "Miei" filter is active) -->
|
||||||
<div *ngIf="canto.id.startsWith('my_')" class="delete-section" (click)="$event.stopPropagation()">
|
<div *ngIf="settingsService.showEditor() && canto.id.startsWith('my_') && showOnlyMine()" class="delete-section" (click)="$event.stopPropagation()">
|
||||||
<ion-button fill="clear" color="danger" (click)="deleteMyCanto(canto.id, $event)">
|
<ion-button fill="clear" color="danger" (click)="deleteMyCanto(canto.id, $event)">
|
||||||
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -443,7 +620,6 @@
|
|||||||
<div class="black-screen-overlay" *ngIf="isBlackOverlayActive()" (click)="isBlackOverlayDismissed.set(true)">
|
<div class="black-screen-overlay" *ngIf="isBlackOverlayActive()" (click)="isBlackOverlayDismissed.set(true)">
|
||||||
<div class="black-screen-content" (click)="$event.stopPropagation()">
|
<div class="black-screen-content" (click)="$event.stopPropagation()">
|
||||||
<div (click)="isBlackOverlayDismissed.set(true)" style="display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; width: 100%;">
|
<div (click)="isBlackOverlayDismissed.set(true)" style="display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; width: 100%;">
|
||||||
<ion-icon name="car-outline" class="car-mode-icon"></ion-icon>
|
|
||||||
<p class="car-mode-text outfit-font">Schermo nero attivo</p>
|
<p class="car-mode-text outfit-font">Schermo nero attivo</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+479
-77
@@ -48,95 +48,447 @@
|
|||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
.voice-search-btn {
|
.adv-search-btn, .voice-search-btn {
|
||||||
--padding-start: 8px;
|
--padding-start: 6px;
|
||||||
--padding-end: 8px;
|
--padding-end: 6px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.adv-search-btn.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-actions-row {
|
.advanced-search-toggle-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
justify-content: space-between;
|
||||||
overflow-x: auto;
|
padding: 0 2px;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
padding: 8px 4px 8px 0;
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
// Hide scrollbar but keep functionality
|
.toggle-row-left {
|
||||||
&::-webkit-scrollbar {
|
display: flex;
|
||||||
display: none;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
.song-count-card {
|
.song-count-card {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
||||||
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);
|
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);
|
||||||
padding: 0 12px;
|
padding: 0 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
height: 32px;
|
height: 30px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-buttons {
|
.advanced-search-link, .clear-advanced-link {
|
||||||
display: flex;
|
background: transparent;
|
||||||
gap: 8px;
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
opacity: 0.9;
|
||||||
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
ion-button.filter-chip {
|
&:hover, &:active {
|
||||||
--border-radius: 20px;
|
opacity: 1;
|
||||||
--border-width: 1px;
|
background: rgba(var(--ion-color-secondary-rgb), 0.12);
|
||||||
font-family: 'Outfit', sans-serif;
|
}
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 32px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: normal;
|
|
||||||
|
|
||||||
.close-icon-wrapper {
|
ion-icon {
|
||||||
display: inline-flex;
|
font-size: 1rem;
|
||||||
align-items: center;
|
}
|
||||||
justify-content: center;
|
|
||||||
margin-left: 6px;
|
.inline-clear-btn {
|
||||||
padding: 4px;
|
display: inline-flex;
|
||||||
margin-right: -8px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--ion-color-danger, #ff4961);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-advanced-link {
|
||||||
|
color: var(--ion-color-danger, #ff4961);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
background: rgba(255, 73, 97, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-search-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
animation: advSearchFadeIn 0.25s ease-out;
|
||||||
|
|
||||||
|
.adv-search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.adv-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 38px;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--ion-color-secondary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--ion-color-secondary-rgb), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-family: inherit;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-clear-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 100;
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-mic-btn {
|
||||||
|
--padding-start: 4px;
|
||||||
|
--padding-end: 4px;
|
||||||
|
margin: 0 0 0 2px;
|
||||||
|
height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 1.2rem;
|
font-size: 1.15rem;
|
||||||
margin: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.5;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes advSearchFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body.high-contrast) {
|
||||||
|
.advanced-search-link {
|
||||||
|
color: var(--ion-color-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-advanced-link {
|
||||||
|
color: var(--ion-color-danger, #d32f2f) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-search-panel {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
|
||||||
|
.adv-input-group {
|
||||||
|
.adv-label {
|
||||||
|
color: var(--ion-color-secondary, #d96b00) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input-wrapper {
|
||||||
|
background: #f4f5f8 !important;
|
||||||
|
border: 1px solid #c0c4cc !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--ion-color-secondary, #d96b00) !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(217, 107, 0, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input-icon {
|
||||||
|
color: var(--ion-color-secondary, #d96b00) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-input {
|
||||||
|
color: #000000 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #666666 !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-clear-icon {
|
||||||
|
color: #666666 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.filter-card-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.filter-row-label {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
width: 68px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-transform: capitalize;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-playlist-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
|
.song-count-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
||||||
|
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
ion-button.filter-chip {
|
||||||
|
--border-radius: 20px;
|
||||||
|
--border-width: 1px;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 32px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
|
||||||
|
.close-icon-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-right: -8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ion-toolbar.playlist-toolbar {
|
||||||
|
--padding-top: 0px;
|
||||||
|
--padding-bottom: 8px;
|
||||||
|
--padding-start: 16px;
|
||||||
|
--padding-end: 16px;
|
||||||
|
--min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-actions-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
|
||||||
|
.playlist-duration-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
||||||
|
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-title-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 50%;
|
||||||
|
margin: 0 8px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
.playlist-title-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-pill {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.transparent-list {
|
.transparent-list {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
padding-bottom: 120px; // Spazio per il player fisso
|
padding-bottom: 120px; // Spazio per il player fisso
|
||||||
@@ -160,28 +512,87 @@ ion-title {
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
|
|
||||||
.add-btn, .settings-btn {
|
|
||||||
--color: var(--ion-color-secondary);
|
|
||||||
--padding-start: 8px;
|
|
||||||
--padding-end: 8px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-header {
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-buttons[slot="end"] {
|
||||||
|
gap: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-btn, .add-btn, .settings-btn {
|
||||||
|
--color: var(--ion-color-secondary);
|
||||||
|
--padding-start: 0;
|
||||||
|
--padding-end: 0;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: 0;
|
||||||
|
width: 38px;
|
||||||
|
min-width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-btn {
|
||||||
|
animation: pulse-update 2.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-update {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(var(--ion-color-secondary-rgb), 0.6));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
ion-buttons[slot="end"] {
|
||||||
|
gap: 0px;
|
||||||
|
}
|
||||||
|
.update-btn, .add-btn, .settings-btn {
|
||||||
|
--padding-start: 0;
|
||||||
|
--padding-end: 0;
|
||||||
|
margin-left: -5px;
|
||||||
|
width: 34px;
|
||||||
|
min-width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.settings-btn {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ion-title {
|
||||||
.header-logo-wrapper {
|
.header-logo-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
padding: 16px 0 16px 24px; // Spacing adjusted for search bar breathing room
|
padding: 10px 0 10px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
width: 42px;
|
width: 38px;
|
||||||
height: 42px;
|
height: 38px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||||
border: 2px solid rgba(var(--ion-color-secondary-rgb), 0.2);
|
border: 2px solid rgba(var(--ion-color-secondary-rgb), 0.2);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text-group {
|
.header-text-group {
|
||||||
@@ -192,24 +603,26 @@ ion-title {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
.app-name {
|
.app-name {
|
||||||
font-size: 1.4rem;
|
font-size: clamp(1.1rem, 3.8vw, 1.25rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-badge {
|
.version-badge {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(255, 255, 255, 0.4);
|
color: rgba(255, 255, 255, 0.4);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.settings-btn {
|
.settings-btn {
|
||||||
margin-right: 16px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.special-list-banner {
|
.special-list-banner {
|
||||||
@@ -775,25 +1188,14 @@ ion-title {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
.play-btn {
|
.play-btn, .skip-btn, .close-btn {
|
||||||
--padding-start: 0;
|
--padding-start: 0;
|
||||||
--padding-end: 0;
|
--padding-end: 0;
|
||||||
height: 44px;
|
height: 40px;
|
||||||
width: 44px;
|
width: 40px;
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 2.2rem;
|
font-size: 1.6rem;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-btn, .close-btn {
|
|
||||||
--padding-start: 0;
|
|
||||||
--padding-end: 0;
|
|
||||||
height: 36px;
|
|
||||||
width: 36px;
|
|
||||||
|
|
||||||
ion-icon {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+491
-34
@@ -19,6 +19,7 @@ import { environment } from '../../environments/environment';
|
|||||||
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
||||||
import { filter, first } from 'rxjs/operators';
|
import { filter, first } from 'rxjs/operators';
|
||||||
import { showFullscreenUpdateOverlay } from '../app.component';
|
import { showFullscreenUpdateOverlay } from '../app.component';
|
||||||
|
import { LyricsParserService } from '../services/lyrics-parser.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -50,6 +51,18 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public searchQuery = signal<string>('');
|
public searchQuery = signal<string>('');
|
||||||
|
public isAdvancedSearchOpen = signal<boolean>(false);
|
||||||
|
public isFilterCardOpen = signal<boolean>(false);
|
||||||
|
public isPlaylistCardOpen = signal<boolean>(false);
|
||||||
|
public searchTitle = signal<string>('');
|
||||||
|
public searchAuthor = signal<string>('');
|
||||||
|
public searchText = signal<string>('');
|
||||||
|
public searchNumber = signal<string>('');
|
||||||
|
public activeVoiceField = signal<'global' | 'title' | 'author' | 'text' | 'number'>('global');
|
||||||
|
|
||||||
|
public hasAdvancedSearchParams = computed(() => {
|
||||||
|
return !!(this.searchTitle().trim() || this.searchAuthor().trim() || this.searchText().trim() || this.searchNumber().trim());
|
||||||
|
});
|
||||||
public selectedLiturgico = signal<number | null>(null);
|
public selectedLiturgico = signal<number | null>(null);
|
||||||
public selectedTematico = signal<number | null>(null);
|
public selectedTematico = signal<number | null>(null);
|
||||||
public showOnlyMine = signal<boolean>(false);
|
public showOnlyMine = signal<boolean>(false);
|
||||||
@@ -57,6 +70,25 @@ export class HomePage implements OnDestroy {
|
|||||||
public showSuggeriti = signal<boolean>(false);
|
public showSuggeriti = signal<boolean>(false);
|
||||||
public showValidati = signal<boolean>(false);
|
public showValidati = signal<boolean>(false);
|
||||||
public showNonValidati = signal<boolean>(false);
|
public showNonValidati = signal<boolean>(false);
|
||||||
|
|
||||||
|
public hasPlaylistCardParams = computed(() => {
|
||||||
|
return !!(
|
||||||
|
this.playlistService.activePlaylistId() !== null ||
|
||||||
|
(this.settingsService.comunitaEnabled() && this.comunitaService.isFilterActive())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
public hasFilterCardParams = computed(() => {
|
||||||
|
return !!(
|
||||||
|
this.selectedLiturgico() !== null ||
|
||||||
|
this.selectedTematico() !== null ||
|
||||||
|
this.showOnlyMine() ||
|
||||||
|
this.showTopTen() ||
|
||||||
|
this.showSuggeriti() ||
|
||||||
|
this.showValidati() ||
|
||||||
|
this.showNonValidati()
|
||||||
|
);
|
||||||
|
});
|
||||||
public isMassCardExpanded = signal<boolean>(false);
|
public isMassCardExpanded = signal<boolean>(false);
|
||||||
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | 'lista_completa' | null>(null);
|
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | 'lista_completa' | null>(null);
|
||||||
public loadedThumbs = new Set<string>();
|
public loadedThumbs = new Set<string>();
|
||||||
@@ -93,6 +125,7 @@ export class HomePage implements OnDestroy {
|
|||||||
public playlistService = inject(PlaylistService);
|
public playlistService = inject(PlaylistService);
|
||||||
public settingsService = inject(SettingsService);
|
public settingsService = inject(SettingsService);
|
||||||
public cantiLettureService = inject(CantiLettureService);
|
public cantiLettureService = inject(CantiLettureService);
|
||||||
|
public lyricsParser = inject(LyricsParserService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
private modalCtrl = inject(ModalController);
|
private modalCtrl = inject(ModalController);
|
||||||
@@ -203,6 +236,24 @@ export class HomePage implements OnDestroy {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[PWA-Update] activateUpdate failed:', e);
|
console.warn('[PWA-Update] activateUpdate failed:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
for (const registration of registrations) {
|
||||||
|
await registration.unregister();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('caches' in window) {
|
||||||
|
const keys = await caches.keys();
|
||||||
|
for (const key of keys) {
|
||||||
|
await caches.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[PWA-Update] Cleanup failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
overlay.finish();
|
overlay.finish();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now());
|
window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now());
|
||||||
@@ -237,17 +288,12 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
private async checkUpdateStatus() {
|
private async checkUpdateStatus() {
|
||||||
try {
|
try {
|
||||||
|
let swFoundUpdate = false;
|
||||||
if (this.swUpdate.isEnabled) {
|
if (this.swUpdate.isEnabled) {
|
||||||
const swFoundUpdate = await this.swUpdate.checkForUpdate().catch(() => false);
|
swFoundUpdate = await this.swUpdate.checkForUpdate().catch(() => false);
|
||||||
if (swFoundUpdate) {
|
|
||||||
this.hasUpdateAvailable.set(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const mismatch = await this.checkVersionJson();
|
const mismatch = await this.checkVersionJson();
|
||||||
if (mismatch) {
|
this.hasUpdateAvailable.set(swFoundUpdate || mismatch);
|
||||||
this.hasUpdateAvailable.set(true);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to check for updates on startup:', err);
|
console.warn('Failed to check for updates on startup:', err);
|
||||||
}
|
}
|
||||||
@@ -377,7 +423,7 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
// Filter by Validati (non personali e non contrassegnati come non validati)
|
// Filter by Validati (non personali e non contrassegnati come non validati)
|
||||||
if (this.showValidati()) {
|
if (this.showValidati()) {
|
||||||
list = list.filter(c => !c.nonValidato && !c.id.startsWith('my_'));
|
list = list.filter(c => !c.nonValidato && (onlyMine || !c.id.startsWith('my_')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by Non-Validati (personali o esplicitamente non validati)
|
// Filter by Non-Validati (personali o esplicitamente non validati)
|
||||||
@@ -413,36 +459,85 @@ export class HomePage implements OnDestroy {
|
|||||||
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
|
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const tokens = query.split(/\s+/).filter(t => t.length > 0);
|
||||||
|
|
||||||
list = list.filter(c => {
|
list = list.filter(c => {
|
||||||
const titleMatch = this.normalize(c.titolo).includes(query);
|
const normTitle = this.normalize(c.titolo);
|
||||||
const authorMatch = this.normalize(c.autore).includes(query);
|
const normAuthor = this.normalize(c.autore);
|
||||||
const commNum = this.getCommunitySongNumber(c);
|
const commNum = this.getCommunitySongNumber(c);
|
||||||
const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query);
|
const songNum = commNum || c.id_canti.toString();
|
||||||
|
|
||||||
const lyricsPlain = (c.testo || '')
|
const lyricsPlain = (c.testo || '')
|
||||||
.replace(/{.*?}/g, '')
|
.replace(/{.*?}/g, '')
|
||||||
.replace(/\n/g, ' ');
|
.replace(/\n/g, ' ');
|
||||||
const lyricsMatch = this.normalize(lyricsPlain).includes(query);
|
const normLyrics = this.normalize(lyricsPlain);
|
||||||
|
|
||||||
return titleMatch || authorMatch || lyricsMatch || numberMatch;
|
// Full string matches in title, author, lyrics, or song number
|
||||||
|
const titleMatch = normTitle.includes(query);
|
||||||
|
const authorMatch = normAuthor.includes(query);
|
||||||
|
const lyricsMatch = normLyrics.includes(query);
|
||||||
|
const numberMatch = songNum.includes(query);
|
||||||
|
|
||||||
|
if (titleMatch || authorMatch || lyricsMatch || numberMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-word token match across fields (e.g. title word + author word)
|
||||||
|
if (tokens.length > 1) {
|
||||||
|
return tokens.every(token =>
|
||||||
|
normTitle.includes(token) ||
|
||||||
|
normAuthor.includes(token) ||
|
||||||
|
normLyrics.includes(token) ||
|
||||||
|
songNum.includes(token)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advanced search filters (titolo, autore, testo, nr canto)
|
||||||
|
const advTitle = this.normalize(this.searchTitle()).trim();
|
||||||
|
const advAuthor = this.normalize(this.searchAuthor()).trim();
|
||||||
|
const advText = this.normalize(this.searchText()).trim();
|
||||||
|
const advNumber = this.normalize(this.searchNumber()).trim();
|
||||||
|
|
||||||
|
if (advTitle) {
|
||||||
|
list = list.filter(c => this.normalize(c.titolo).includes(advTitle));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advAuthor) {
|
||||||
|
list = list.filter(c => this.normalize(c.autore || '').includes(advAuthor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advText) {
|
||||||
|
list = list.filter(c => {
|
||||||
|
const lyricsPlain = (c.testo || '')
|
||||||
|
.replace(/{.*?}/g, '')
|
||||||
|
.replace(/\n/g, ' ');
|
||||||
|
return this.normalize(lyricsPlain).includes(advText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advNumber) {
|
||||||
|
list = list.filter(c => {
|
||||||
|
const commNum = this.getCommunitySongNumber(c);
|
||||||
|
const songNum = (commNum ? commNum.toString() : c.id_canti.toString()).toLowerCase().trim();
|
||||||
|
return songNum === advNumber.toLowerCase().trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// G. Sorting/Ordering
|
// G. Sorting/Ordering
|
||||||
if (topTen) {
|
if (topTen) {
|
||||||
const eseguiti = this.cantiService.cantiEseguiti();
|
list = [...list].sort((a, b) => {
|
||||||
const eseguitiMap = new Map<number, number>();
|
const numA = this.getEsecuzioniCount(a.id) || 0;
|
||||||
eseguiti.forEach(x => eseguitiMap.set(x.id_canti, x.num));
|
const numB = this.getEsecuzioniCount(b.id) || 0;
|
||||||
|
|
||||||
list = list.sort((a, b) => {
|
|
||||||
const numA = eseguitiMap.get(a.id_canti) || 0;
|
|
||||||
const numB = eseguitiMap.get(b.id_canti) || 0;
|
|
||||||
return numB - numA;
|
return numB - numA;
|
||||||
});
|
});
|
||||||
} else if (suggeriti) {
|
} else if (suggeriti) {
|
||||||
const suggMap = this.cantiLettureService.suggestionsMap();
|
const suggMap = this.cantiLettureService.suggestionsMap();
|
||||||
list = list.sort((a, b) => {
|
list = [...list].sort((a, b) => {
|
||||||
const pesoA = suggMap.get(a.id_canti) || 0;
|
const pesoA = suggMap.get(a.id_canti) || 0;
|
||||||
const pesoB = suggMap.get(b.id_canti) || 0;
|
const pesoB = suggMap.get(b.id_canti) || 0;
|
||||||
return pesoB - pesoA;
|
return pesoB - pesoA;
|
||||||
@@ -462,6 +557,63 @@ export class HomePage implements OnDestroy {
|
|||||||
return this.filteredCanti().slice(0, this.limit());
|
return this.filteredCanti().slice(0, this.limit());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public hasAnyFilter = computed(() => {
|
||||||
|
return (
|
||||||
|
this.playlistService.activeListName() !== null ||
|
||||||
|
this.selectedLiturgico() !== null ||
|
||||||
|
this.selectedTematico() !== null ||
|
||||||
|
this.showOnlyMine() ||
|
||||||
|
this.showTopTen() ||
|
||||||
|
this.showSuggeriti() ||
|
||||||
|
this.showValidati() ||
|
||||||
|
this.showNonValidati() ||
|
||||||
|
!!this.searchQuery().trim() ||
|
||||||
|
this.hasAdvancedSearchParams() ||
|
||||||
|
(this.settingsService.comunitaEnabled() && this.comunitaService.isFilterActive())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
public totalPlaylistDuration = computed(() => {
|
||||||
|
if (this.playlistService.activePlaylistId() === null) return '';
|
||||||
|
const songs = this.filteredCanti();
|
||||||
|
if (songs.length === 0) return '';
|
||||||
|
|
||||||
|
let totalSeconds = 0;
|
||||||
|
for (const song of songs) {
|
||||||
|
if (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() {
|
constructor() {
|
||||||
// Check if there is an update available
|
// Check if there is an update available
|
||||||
this.checkUpdateStatus();
|
this.checkUpdateStatus();
|
||||||
@@ -476,8 +628,15 @@ export class HomePage implements OnDestroy {
|
|||||||
// Polling setup: check for updates every 30 seconds
|
// Polling setup: check for updates every 30 seconds
|
||||||
this.updatePollInterval = setInterval(() => {
|
this.updatePollInterval = setInterval(() => {
|
||||||
this.checkUpdateStatus();
|
this.checkUpdateStatus();
|
||||||
|
if (this.isRemotePlaylist()) {
|
||||||
|
this.playlistService.checkForRemotePlaylistUpdates();
|
||||||
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
// Sync homepage filtered canti IDs to playlistService.filteredListIds
|
||||||
|
effect(() => {
|
||||||
|
const ids = this.filteredCanti().map(c => c.id);
|
||||||
|
this.playlistService.filteredListIds.set(ids);
|
||||||
|
}, { allowSignalWrites: true });
|
||||||
|
|
||||||
|
|
||||||
// Track initial community filter state to avoid clearing during the initial run of the effect
|
// Track initial community filter state to avoid clearing during the initial run of the effect
|
||||||
@@ -509,11 +668,29 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}, { allowSignalWrites: true });
|
}, { allowSignalWrites: true });
|
||||||
|
|
||||||
// Sync speech recognition results to search query
|
// Sync speech recognition results to search query or advanced field
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const transcript = this.audioEngine.searchTranscript();
|
const transcript = this.audioEngine.searchTranscript();
|
||||||
if (transcript) {
|
if (transcript) {
|
||||||
this.searchQuery.set(transcript);
|
const target = this.activeVoiceField();
|
||||||
|
switch (target) {
|
||||||
|
case 'title':
|
||||||
|
this.searchTitle.set(transcript);
|
||||||
|
break;
|
||||||
|
case 'author':
|
||||||
|
this.searchAuthor.set(transcript);
|
||||||
|
break;
|
||||||
|
case 'text':
|
||||||
|
this.searchText.set(transcript);
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
this.searchNumber.set(transcript);
|
||||||
|
break;
|
||||||
|
case 'global':
|
||||||
|
default:
|
||||||
|
this.searchQuery.set(transcript);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -631,6 +808,15 @@ export class HomePage implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ionViewWillEnter() {
|
||||||
|
// Gestione dati QR scansionati e passati via router state dalle Impostazioni
|
||||||
|
const navigation = this.router.getCurrentNavigation();
|
||||||
|
const state = navigation?.extras.state as { scannedQrData?: string } | undefined;
|
||||||
|
if (state?.scannedQrData) {
|
||||||
|
this.handleScannedData(state.scannedQrData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async handleImport(base64: string) {
|
async handleImport(base64: string) {
|
||||||
@@ -781,7 +967,9 @@ export class HomePage implements OnDestroy {
|
|||||||
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
||||||
link_youtube: item.link_youtube || '',
|
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)) || [],
|
||||||
nonValidato: true
|
nonValidato: true,
|
||||||
|
durata: item.durata || '',
|
||||||
|
bpm: item.bpm !== undefined && item.bpm !== null ? Number(item.bpm) : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.playlistService.saveRemoteShareCanto(canto);
|
await this.playlistService.saveRemoteShareCanto(canto);
|
||||||
@@ -839,7 +1027,9 @@ export class HomePage implements OnDestroy {
|
|||||||
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
||||||
autore: item.autore || '',
|
autore: item.autore || '',
|
||||||
link_youtube: item.link_youtube || '',
|
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
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 2. Reconstruct playlists
|
// 2. Reconstruct playlists
|
||||||
@@ -884,6 +1074,7 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
// Select it immediately
|
// Select it immediately
|
||||||
this.selectPlaylist(selectedPl);
|
this.selectPlaylist(selectedPl);
|
||||||
|
this.playlistService.lastPlaylist.set(selectedPl);
|
||||||
this.activeFilterType.set('playlist');
|
this.activeFilterType.set('playlist');
|
||||||
|
|
||||||
await loading.dismiss();
|
await loading.dismiss();
|
||||||
@@ -954,7 +1145,9 @@ export class HomePage implements OnDestroy {
|
|||||||
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
||||||
autore: item.autore || '',
|
autore: item.autore || '',
|
||||||
link_youtube: item.link_youtube || '',
|
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) {
|
if (customSongs.length > 0) {
|
||||||
@@ -1036,6 +1229,95 @@ export class HomePage implements OnDestroy {
|
|||||||
this.limit.set(30);
|
this.limit.set(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleAdvancedSearch() {
|
||||||
|
const willOpen = !this.isAdvancedSearchOpen();
|
||||||
|
this.isAdvancedSearchOpen.set(willOpen);
|
||||||
|
if (willOpen) {
|
||||||
|
this.isFilterCardOpen.set(false);
|
||||||
|
this.isPlaylistCardOpen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFilterCard() {
|
||||||
|
const willOpen = !this.isFilterCardOpen();
|
||||||
|
this.isFilterCardOpen.set(willOpen);
|
||||||
|
if (willOpen) {
|
||||||
|
this.isAdvancedSearchOpen.set(false);
|
||||||
|
this.isPlaylistCardOpen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlaylistCard() {
|
||||||
|
const willOpen = !this.isPlaylistCardOpen();
|
||||||
|
this.isPlaylistCardOpen.set(willOpen);
|
||||||
|
if (willOpen) {
|
||||||
|
this.isAdvancedSearchOpen.set(false);
|
||||||
|
this.isFilterCardOpen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAdvancedSearch(event?: Event) {
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.searchTitle.set('');
|
||||||
|
this.searchAuthor.set('');
|
||||||
|
this.searchText.set('');
|
||||||
|
this.searchNumber.set('');
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFilterCard(event?: Event) {
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.selectedLiturgico.set(null);
|
||||||
|
this.selectedTematico.set(null);
|
||||||
|
this.showOnlyMine.set(false);
|
||||||
|
this.showTopTen.set(false);
|
||||||
|
this.showSuggeriti.set(false);
|
||||||
|
this.showValidati.set(false);
|
||||||
|
this.showNonValidati.set(false);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPlaylistCard(event?: Event) {
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.playlistService.activeListIds.set([]);
|
||||||
|
this.playlistService.activeListName.set(null);
|
||||||
|
this.playlistService.activePlaylistId.set(null);
|
||||||
|
if (this.settingsService.comunitaEnabled() && this.comunitaService.isFilterActive()) {
|
||||||
|
this.toggleComunitaFilter();
|
||||||
|
}
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchTitleInput(event: any) {
|
||||||
|
const val = event.target?.value ?? event.detail?.value ?? '';
|
||||||
|
this.searchTitle.set(val);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchAuthorInput(event: any) {
|
||||||
|
const val = event.target?.value ?? event.detail?.value ?? '';
|
||||||
|
this.searchAuthor.set(val);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchTextInput(event: any) {
|
||||||
|
const val = event.target?.value ?? event.detail?.value ?? '';
|
||||||
|
this.searchText.set(val);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchNumberInput(event: any) {
|
||||||
|
const val = event.target?.value ?? event.detail?.value ?? '';
|
||||||
|
this.searchNumber.set(val);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
async deleteMyCanto(id: string, event: Event) {
|
async deleteMyCanto(id: string, event: Event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
@@ -1195,7 +1477,8 @@ export class HomePage implements OnDestroy {
|
|||||||
this.showValidati() ||
|
this.showValidati() ||
|
||||||
this.showNonValidati() ||
|
this.showNonValidati() ||
|
||||||
this.playlistService.activeListName() !== null ||
|
this.playlistService.activeListName() !== null ||
|
||||||
this.searchQuery() !== '';
|
this.searchQuery() !== '' ||
|
||||||
|
this.hasAdvancedSearchParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldShowSubSectionToolbar(): boolean {
|
shouldShowSubSectionToolbar(): boolean {
|
||||||
@@ -1221,6 +1504,7 @@ export class HomePage implements OnDestroy {
|
|||||||
this.playlistService.activeListName.set(null);
|
this.playlistService.activeListName.set(null);
|
||||||
this.playlistService.activePlaylistId.set(null);
|
this.playlistService.activePlaylistId.set(null);
|
||||||
this.searchQuery.set('');
|
this.searchQuery.set('');
|
||||||
|
this.clearAdvancedSearch();
|
||||||
this.activeFilterType.set(null);
|
this.activeFilterType.set(null);
|
||||||
this.limit.set(10);
|
this.limit.set(10);
|
||||||
}
|
}
|
||||||
@@ -1251,6 +1535,11 @@ export class HomePage implements OnDestroy {
|
|||||||
this.playlistService.activeListIds.set(pl.ids);
|
this.playlistService.activeListIds.set(pl.ids);
|
||||||
this.playlistService.activeListName.set(pl.name);
|
this.playlistService.activeListName.set(pl.name);
|
||||||
this.playlistService.activePlaylistId.set(pl.id);
|
this.playlistService.activePlaylistId.set(pl.id);
|
||||||
|
if (pl.isRemote || pl.id?.startsWith('remote_')) {
|
||||||
|
this.playlistService.checkForRemotePlaylistUpdates();
|
||||||
|
}
|
||||||
|
this.isAdvancedSearchOpen.set(false);
|
||||||
|
this.isFilterCardOpen.set(false);
|
||||||
// Mantieni il menu aperto poiché la playlist è ora selezionata ed attiva
|
// Mantieni il menu aperto poiché la playlist è ora selezionata ed attiva
|
||||||
this.limit.set(50);
|
this.limit.set(50);
|
||||||
}
|
}
|
||||||
@@ -1656,10 +1945,16 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVoiceSearch() {
|
toggleVoiceSearch(target: 'global' | 'title' | 'author' | 'text' | 'number' = 'global') {
|
||||||
if (this.audioEngine.isSearching()) {
|
if (this.audioEngine.isSearching()) {
|
||||||
|
const currentTarget = this.activeVoiceField();
|
||||||
this.audioEngine.stopSearchRecognition();
|
this.audioEngine.stopSearchRecognition();
|
||||||
|
if (currentTarget !== target) {
|
||||||
|
this.activeVoiceField.set(target);
|
||||||
|
this.audioEngine.startSearchRecognition();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this.activeVoiceField.set(target);
|
||||||
this.audioEngine.startSearchRecognition();
|
this.audioEngine.startSearchRecognition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1730,18 +2025,57 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
isComunitaPlaylist(): boolean {
|
isComunitaPlaylist(): boolean {
|
||||||
const id = this.playlistService.activePlaylistId();
|
const id = this.playlistService.activePlaylistId();
|
||||||
return !!id && id.startsWith('comunita_');
|
if (id && id.startsWith('comunita_')) return true;
|
||||||
|
const name = this.playlistService.activeListName();
|
||||||
|
if (name) {
|
||||||
|
const pl = this.playlistService.allPlaylists().find(p => p.name === name);
|
||||||
|
return !!pl?.isComunita;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isRemotePlaylist(): boolean {
|
isRemotePlaylist(): boolean {
|
||||||
const id = this.playlistService.activePlaylistId();
|
const id = this.playlistService.activePlaylistId();
|
||||||
return !!id && id.startsWith('remote_');
|
if (id && id.startsWith('remote_')) return true;
|
||||||
|
const name = this.playlistService.activeListName();
|
||||||
|
if (name) {
|
||||||
|
const pl = this.playlistService.allPlaylists().find(p => p.name === name);
|
||||||
|
return !!pl?.isRemote;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
isActivePlaylistSaved(): boolean {
|
||||||
const id = this.playlistService.activePlaylistId();
|
const id = this.playlistService.activePlaylistId();
|
||||||
if (!id) return false;
|
if (id) return this.playlistService.playlists().some(p => p.id === id);
|
||||||
return this.playlistService.playlists().some(p => p.id === id);
|
const name = this.playlistService.activeListName();
|
||||||
|
if (name) return this.playlistService.playlists().some(p => p.name === name);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSpecialList(event?: Event) {
|
clearSpecialList(event?: Event) {
|
||||||
@@ -1771,6 +2105,59 @@ export class HomePage implements OnDestroy {
|
|||||||
this.playlistService.sharePlaylistQR(ids, name, songSettings);
|
this.playlistService.sharePlaylistQR(ids, name, songSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async printActivePlaylist() {
|
||||||
|
const ids = this.playlistService.activeListIds();
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
|
const allCanti = this.cantiService.canti();
|
||||||
|
const myCantiList = this.myCantiService.myCanti();
|
||||||
|
const comunitaCantiPers = this.comunitaService.comunitaCantiPersonali();
|
||||||
|
const communityActive = this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive();
|
||||||
|
|
||||||
|
const songs = ids.map(id => {
|
||||||
|
let found: any = null;
|
||||||
|
if (communityActive) {
|
||||||
|
found = comunitaCantiPers.find(c => c.id === id);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
found = allCanti.find(c => c.id === id);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
found = myCantiList.find(c => c.id === id);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
found = comunitaCantiPers.find(c => c.id === id);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}).filter(c => !!c);
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Esporta in PDF',
|
||||||
|
message: 'Vuoi includere gli accordi nel PDF o esportare solo il testo?',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Solo Testo',
|
||||||
|
handler: () => {
|
||||||
|
const name = this.playlistService.activeListName() || 'Playlist';
|
||||||
|
this.playlistService.printPlaylist(name, songs, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Testo e Accordi',
|
||||||
|
handler: () => {
|
||||||
|
const name = this.playlistService.activeListName() || 'Playlist';
|
||||||
|
this.playlistService.printPlaylist(name, songs, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
async cloneActivePlaylist() {
|
async cloneActivePlaylist() {
|
||||||
const id = this.playlistService.activePlaylistId();
|
const id = this.playlistService.activePlaylistId();
|
||||||
const currentName = this.playlistService.activeListName() || 'Playlist';
|
const currentName = this.playlistService.activeListName() || 'Playlist';
|
||||||
@@ -1807,7 +2194,9 @@ export class HomePage implements OnDestroy {
|
|||||||
color: 'success'
|
color: 'success'
|
||||||
});
|
});
|
||||||
await toast.present();
|
await toast.present();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1925,6 +2314,44 @@ export class HomePage implements OnDestroy {
|
|||||||
return index !== -1 ? index + 1 : 0;
|
return index !== -1 ? index + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSongTonality(canto: any): string | null {
|
||||||
|
if (!canto) return null;
|
||||||
|
const baseKey = this.lyricsParser.deduceTonality(canto.accordi || canto.testo || '');
|
||||||
|
if (!baseKey) return null;
|
||||||
|
|
||||||
|
let semitones = 0;
|
||||||
|
const activePlaylistId = this.playlistService.activePlaylistId();
|
||||||
|
if (activePlaylistId) {
|
||||||
|
let playlistSongSetting: any = null;
|
||||||
|
if (activePlaylistId.startsWith('remote_')) {
|
||||||
|
const pl = this.playlistService.remotePlaylist();
|
||||||
|
if (pl && pl.songSettings && pl.songSettings[canto.id]) {
|
||||||
|
playlistSongSetting = pl.songSettings[canto.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pl = this.playlistService.playlists().find(p => p.id === activePlaylistId);
|
||||||
|
if (pl && pl.songSettings && pl.songSettings[canto.id]) {
|
||||||
|
playlistSongSetting = pl.songSettings[canto.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (playlistSongSetting && playlistSongSetting.tonalita !== undefined) {
|
||||||
|
semitones = playlistSongSetting.tonalita;
|
||||||
|
}
|
||||||
|
} else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) {
|
||||||
|
const settings = this.comunitaService.comunitaCantiSettings();
|
||||||
|
const songSetting = settings.find(s => s.id_canti === canto.id_canti || s.id_canti === Number(canto.id));
|
||||||
|
if (songSetting && songSetting.tonalita !== undefined) {
|
||||||
|
semitones = songSetting.tonalita;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semitones !== 0) {
|
||||||
|
return this.lyricsParser.transposeChord(baseKey, semitones);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseKey;
|
||||||
|
}
|
||||||
|
|
||||||
toggleComunitaFilter() {
|
toggleComunitaFilter() {
|
||||||
if (this.comunitaService.comunitaCode()) {
|
if (this.comunitaService.comunitaCode()) {
|
||||||
this.comunitaService.isFilterActive.update(v => !v);
|
this.comunitaService.isFilterActive.update(v => !v);
|
||||||
@@ -1966,6 +2393,36 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private panelStartX: number = 0;
|
||||||
|
private panelStartY: number = 0;
|
||||||
|
|
||||||
|
onPanelTouchStart(event: TouchEvent) {
|
||||||
|
if (event.touches.length === 1) {
|
||||||
|
this.panelStartX = event.touches[0].clientX;
|
||||||
|
this.panelStartY = event.touches[0].clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPanelTouchEnd(event: TouchEvent, panelType: 'advancedSearch' | 'playlistCard' | 'filterCard') {
|
||||||
|
if (event.changedTouches.length === 1) {
|
||||||
|
const endX = event.changedTouches[0].clientX;
|
||||||
|
const endY = event.changedTouches[0].clientY;
|
||||||
|
const diffX = endX - this.panelStartX;
|
||||||
|
const diffY = endY - this.panelStartY;
|
||||||
|
|
||||||
|
// Swipe UP: vertical movement upwards (diffY negative, e.g. < -40) and larger than horizontal diff
|
||||||
|
if (diffY < -40 && Math.abs(diffY) > Math.abs(diffX)) {
|
||||||
|
if (panelType === 'advancedSearch') {
|
||||||
|
this.isAdvancedSearchOpen.set(false);
|
||||||
|
} else if (panelType === 'playlistCard') {
|
||||||
|
this.isPlaylistCardOpen.set(false);
|
||||||
|
} else if (panelType === 'filterCard') {
|
||||||
|
this.isFilterCardOpen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async navigateMassDate(direction: number) {
|
async navigateMassDate(direction: number) {
|
||||||
const list = this.cantiLettureService.availableMasses();
|
const list = this.cantiLettureService.availableMasses();
|
||||||
if (list.length === 0) return;
|
if (list.length === 0) return;
|
||||||
|
|||||||
@@ -1,37 +1,70 @@
|
|||||||
<ion-header [translucent]="true" class="ion-no-border">
|
<ion-header [translucent]="true" class="ion-no-border">
|
||||||
<ion-toolbar class="bg-gradient top-toolbar">
|
<ion-toolbar class="bg-gradient top-toolbar" style="--padding-top: 8px; --padding-bottom: 8px; --padding-start: 12px; --padding-end: 12px;">
|
||||||
<ion-buttons slot="start">
|
<div class="outfit-font" style="display: flex; flex-direction: column; width: 100%; gap: 2px;">
|
||||||
<ion-back-button text="" defaultHref="/home" color="secondary"></ion-back-button>
|
|
||||||
</ion-buttons>
|
<!-- Row 1: Number + Title + Settings -->
|
||||||
<ion-title class="outfit-font wrapped-title">
|
<div style="display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;">
|
||||||
<div class="title-main" [style.fontSize.rem]="fontSize() * 1.1">
|
<span class="canto-number" *ngIf="canto()?.id_canti" style="flex-shrink: 0;"
|
||||||
<span class="canto-number" *ngIf="canto()?.id_canti">
|
[style.fontSize.rem]="0.85 * fontSize()"
|
||||||
|
[style.width.px]="32 * fontSize()"
|
||||||
|
[style.height.px]="32 * fontSize()"
|
||||||
|
[style.lineHeight.px]="28 * fontSize()"
|
||||||
|
[style.borderRadius.px]="8 * fontSize()">
|
||||||
{{ canto()?.id?.startsWith('my_') ? getMySongNumber(canto()) : canto()?.id_canti }}
|
{{ canto()?.id?.startsWith('my_') ? getMySongNumber(canto()) : canto()?.id_canti }}
|
||||||
</span>
|
</span>
|
||||||
<span class="title-text" style="display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
<span class="canto-title"
|
||||||
<span *ngIf="getCommunitySongNumber(canto())" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto()) }}</span>
|
[style.fontSize.rem]="1.15 * fontSize()"
|
||||||
|
style="font-weight: 700; 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>{{ canto()?.titolo || 'Player' }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
<ion-button routerLink="/settings" class="settings-btn" fill="clear" style="flex-shrink: 0; margin: 0; --color: var(--ion-color-secondary);">
|
||||||
|
<ion-icon slot="icon-only" name="settings-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
</ion-title>
|
|
||||||
<ion-buttons slot="end">
|
<!-- Row 2: Author -->
|
||||||
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
<div *ngIf="!isLandscapeActive()"
|
||||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
style="display: block; width: 100%; margin-top: 1px; margin-bottom: 2px;"
|
||||||
|
[style.paddingLeft.px]="canto()?.id_canti ? (32 * fontSize() + 8) : 0">
|
||||||
|
<span class="song-author"
|
||||||
|
[style.fontSize.rem]="0.95 * fontSize()"
|
||||||
|
style="color: var(--ion-text-color); opacity: 0.8; font-weight: 400; text-align: left; display: block;">
|
||||||
|
{{ canto()?.autore || 'Autore sconosciuto' }}
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
<!-- Row 3: Badges only -->
|
||||||
</ion-button>
|
<div *ngIf="!isLandscapeActive()" class="scroll-horizontal"
|
||||||
<ion-button fill="clear" (click)="shareCanto()" *ngIf="!isLandscapeActive()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
|
style="margin-top: 2px; width: 100%;"
|
||||||
<ion-icon slot="icon-only" name="share-social-outline" color="secondary" style="font-size: 1.3rem;"></ion-icon>
|
[style.paddingLeft.px]="canto()?.id_canti ? (32 * fontSize() + 8) : 0">
|
||||||
</ion-button>
|
<div style="display: flex; align-items: center; gap: inherit;">
|
||||||
<ion-button fill="clear" (click)="toggleChords()" *ngIf="!isLandscapeActive()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
|
<ng-container *ngIf="settingsService.showDurationBpmTonality()">
|
||||||
<ion-icon slot="icon-only"
|
<span *ngIf="canto()?.durata" class="song-duration-badge"
|
||||||
[name]="showChords() ? 'musical-notes-outline' : 'text-outline'"
|
[style.fontSize.rem]="0.75 * fontSize()"
|
||||||
[color]="showChords() ? 'secondary' : 'medium'"
|
[style.height.px]="26 * fontSize()"
|
||||||
style="font-size: 1.3rem;">
|
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>
|
<ion-icon name="time-outline" [style.fontSize.rem]="1.05 * fontSize()"></ion-icon>
|
||||||
</ion-button>
|
{{ canto()?.durata }}
|
||||||
</ion-buttons>
|
</span>
|
||||||
|
<span *ngIf="canto()?.bpm" class="song-bpm-badge"
|
||||||
|
[style.fontSize.rem]="0.75 * fontSize()"
|
||||||
|
[style.height.px]="26 * fontSize()"
|
||||||
|
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" [style.fontSize.rem]="1.05 * fontSize()"></ion-icon>
|
||||||
|
{{ canto()?.bpm }} BPM
|
||||||
|
</span>
|
||||||
|
<span *ngIf="tonality()" class="song-tonality-badge"
|
||||||
|
[style.fontSize.rem]="0.75 * fontSize()"
|
||||||
|
[style.height.px]="26 * fontSize()"
|
||||||
|
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" [style.fontSize.rem]="1.05 * fontSize()"></ion-icon>
|
||||||
|
{{ tonality() }}
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|
||||||
<!-- Audio Toolbar Removed -->
|
<!-- Audio Toolbar Removed -->
|
||||||
@@ -105,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sections rendering -->
|
<!-- Sections rendering -->
|
||||||
<div class="lyrics-view">
|
<div class="lyrics-view" (click)="handleLyricsClick($event)">
|
||||||
<div *ngFor="let section of parsedSections(); let si = index"
|
<div *ngFor="let section of parsedSections(); let si = index"
|
||||||
class="section"
|
class="section"
|
||||||
[class.chorus]="section.type === 'chorus'"
|
[class.chorus]="section.type === 'chorus'"
|
||||||
@@ -145,6 +178,9 @@
|
|||||||
<ion-toolbar class="global-player-toolbar glass">
|
<ion-toolbar class="global-player-toolbar glass">
|
||||||
<div class="player-content">
|
<div class="player-content">
|
||||||
<div class="controls-row">
|
<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-button fill="clear" color="secondary" (click)="prevSong()" class="skip-btn">
|
||||||
<ion-icon slot="icon-only" name="chevron-back"></ion-icon>
|
<ion-icon slot="icon-only" name="chevron-back"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -209,7 +245,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation (Avanzamento Karaoke e Riparti da inizio) -->
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<ion-button fill="clear" size="small" (click)="restart()">
|
<ion-button fill="clear" size="small" (click)="restart()">
|
||||||
<ion-icon slot="icon-only" name="arrow-up-circle" color="secondary"></ion-icon>
|
<ion-icon slot="icon-only" name="arrow-up-circle" color="secondary"></ion-icon>
|
||||||
@@ -222,6 +258,28 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
</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)="deleteMyCanto()" *ngIf="settingsService.showEditor() && canto()?.id?.startsWith('my_')" color="danger">
|
||||||
|
<ion-icon slot="icon-only" name="trash-outline"></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>
|
||||||
|
<ion-button fill="clear" size="small" (click)="printCanto()" title="Esporta PDF">
|
||||||
|
<ion-icon slot="icon-only" name="print-outline" color="secondary"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Transposition (Only in chords mode) -->
|
<!-- Transposition (Only in chords mode) -->
|
||||||
<div class="group" *ngIf="showChords()">
|
<div class="group" *ngIf="showChords()">
|
||||||
<ion-button fill="clear" size="small" (click)="transposeDown()">
|
<ion-button fill="clear" size="small" (click)="transposeDown()">
|
||||||
@@ -229,7 +287,8 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
<div class="transpose-indicator">
|
<div class="transpose-indicator">
|
||||||
<ion-icon name="musical-note" color="secondary"></ion-icon>
|
<ion-icon name="musical-note" color="secondary"></ion-icon>
|
||||||
<span class="val" *ngIf="transposeAmount() !== 0">{{ transposeAmount() > 0 ? '+' : '' }}{{ transposeAmount() }}</span>
|
<span class="val" *ngIf="tonality()">{{ tonality() }}</span>
|
||||||
|
<span class="val" *ngIf="transposeAmount() !== 0" style="opacity: 0.8; font-size: 0.75rem; margin-left: 2px;">({{ transposeAmount() > 0 ? '+' : '' }}{{ transposeAmount() }})</span>
|
||||||
</div>
|
</div>
|
||||||
<ion-button fill="clear" size="small" (click)="transposeUp()">
|
<ion-button fill="clear" size="small" (click)="transposeUp()">
|
||||||
<ion-icon slot="icon-only" name="add"></ion-icon>
|
<ion-icon slot="icon-only" name="add"></ion-icon>
|
||||||
@@ -266,7 +325,6 @@
|
|||||||
<div class="black-screen-overlay" *ngIf="isBlackScreen()" (click)="deactivateBlackScreen()">
|
<div class="black-screen-overlay" *ngIf="isBlackScreen()" (click)="deactivateBlackScreen()">
|
||||||
<div class="black-screen-content" (click)="$event.stopPropagation()">
|
<div class="black-screen-content" (click)="$event.stopPropagation()">
|
||||||
<div (click)="deactivateBlackScreen()" style="display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; width: 100%;">
|
<div (click)="deactivateBlackScreen()" style="display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; width: 100%;">
|
||||||
<ion-icon name="car-outline" class="car-mode-icon"></ion-icon>
|
|
||||||
<p class="car-mode-text outfit-font">Schermo nero attivo</p>
|
<p class="car-mode-text outfit-font">Schermo nero attivo</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
background: radial-gradient(circle at top left, #2d3436 0%, #121212 100%);
|
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 {
|
.outfit-font {
|
||||||
font-family: 'Outfit', sans-serif;
|
font-family: 'Outfit', sans-serif;
|
||||||
}
|
}
|
||||||
@@ -20,40 +27,71 @@
|
|||||||
text-align: left !important;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
--color: var(--ion-color-secondary);
|
||||||
|
--padding-start: 0;
|
||||||
|
--padding-end: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
width: 38px;
|
||||||
|
min-width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title-main {
|
.title-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
font-size: 1.15rem; // Fisso: non influenzato dallo zoom del testo
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
.canto-number {
|
.song-author {
|
||||||
font-size: 0.85em;
|
font-size: 0.75rem;
|
||||||
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
color: rgba(255, 255, 255, 0.55);
|
||||||
padding: 2px 10px;
|
font-weight: 400;
|
||||||
display: inline-flex;
|
display: inline-block;
|
||||||
align-items: center;
|
font-family: 'Outfit', sans-serif;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
@@ -61,6 +99,102 @@
|
|||||||
text-align: left;
|
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
|
// Content and Lyrics
|
||||||
@@ -105,25 +239,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
.play-btn {
|
.play-btn, .skip-btn, .close-btn {
|
||||||
--padding-start: 0;
|
--padding-start: 0;
|
||||||
--padding-end: 0;
|
--padding-end: 0;
|
||||||
height: 44px;
|
height: 40px;
|
||||||
width: 44px;
|
width: 40px;
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 2.2rem;
|
font-size: 1.6rem;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-btn, .close-btn {
|
|
||||||
--padding-start: 0;
|
|
||||||
--padding-end: 0;
|
|
||||||
height: 36px;
|
|
||||||
width: 36px;
|
|
||||||
|
|
||||||
ion-icon {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +267,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.chorus {
|
&.chorus {
|
||||||
@@ -160,21 +283,22 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.25rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyric-line {
|
.lyric-line {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.4rem;
|
||||||
line-height: 1.6;
|
line-height: 1.35;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
min-height: 1.5em;
|
min-height: 1.5em;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
@@ -731,6 +855,10 @@ ion-content.full-screen-content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host-context(body.high-contrast) {
|
:host-context(body.high-contrast) {
|
||||||
|
.title-main .song-author {
|
||||||
|
color: rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.slim-toolbar {
|
.slim-toolbar {
|
||||||
--background: #ffffff !important;
|
--background: #ffffff !important;
|
||||||
background: #ffffff !important;
|
background: #ffffff !important;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { FaceDetectorService } from '../../services/face-detector.service';
|
|||||||
export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||||
public canto = signal<Canto | null>(null);
|
public canto = signal<Canto | null>(null);
|
||||||
public activeSongId = signal<string | null>(null);
|
public activeSongId = signal<string | null>(null);
|
||||||
|
public isTitleWrapped = signal<boolean>(false);
|
||||||
|
|
||||||
@HostListener('window:keydown', ['$event'])
|
@HostListener('window:keydown', ['$event'])
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: KeyboardEvent) {
|
||||||
@@ -42,6 +43,30 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
} else if (event.key === 'MediaPlayPause') {
|
} else if (event.key === 'MediaPlayPause') {
|
||||||
this.toggleAudio();
|
this.toggleAudio();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
} else if (event.key === 'PageUp' || event.key === 'ArrowLeft') {
|
||||||
|
this.prev(true);
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.key === 'PageDown' || event.key === 'ArrowRight') {
|
||||||
|
this.next(false, true);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLyricsClick(event: MouseEvent) {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (target && (target.closest('button') || target.closest('ion-button') || target.closest('.side-indicator') || target.closest('.autoscroll-indicator') || target.closest('ion-icon'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.toString().length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clickX = event.clientX;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
if (clickX > width / 2) {
|
||||||
|
this.next(false, true);
|
||||||
|
} else {
|
||||||
|
this.prev(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +133,17 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
public transposeAmount = signal<number>(0);
|
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 currentLineIndex = signal<number>(0);
|
||||||
public math = Math;
|
public math = Math;
|
||||||
public youtubePlayerService = inject(YoutubePlayerService);
|
public youtubePlayerService = inject(YoutubePlayerService);
|
||||||
@@ -155,7 +191,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
public faceDetector = inject(FaceDetectorService);
|
public faceDetector = inject(FaceDetectorService);
|
||||||
private meta = inject(Meta);
|
private meta = inject(Meta);
|
||||||
|
|
||||||
public enableCameraNavigation = signal<boolean>(false);
|
public enableCameraNavigation = computed(() => this.settingsService.cameraNavigationActive());
|
||||||
|
|
||||||
private songStartTime: number = 0;
|
private songStartTime: number = 0;
|
||||||
|
|
||||||
@@ -192,6 +228,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.playlistService.autoPlayPlaylist()) {
|
if (this.playlistService.autoPlayPlaylist()) {
|
||||||
setTimeout(() => this.initPlayer(c.id), 500);
|
setTimeout(() => this.initPlayer(c.id), 500);
|
||||||
}
|
}
|
||||||
|
this.isTitleWrapped.set(false);
|
||||||
|
setTimeout(() => this.checkTitleWrap(), 150);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,13 +327,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (playlistSongSetting) {
|
if (playlistSongSetting) {
|
||||||
this.transposeAmount.set(playlistSongSetting.tonalita !== undefined ? playlistSongSetting.tonalita : 0);
|
this.transposeAmount.set(playlistSongSetting.tonalita !== undefined ? playlistSongSetting.tonalita : 0);
|
||||||
this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2);
|
this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2);
|
||||||
if (playlistSongSetting.zoom !== undefined) {
|
const gZoom = this.settingsService.globalZoom();
|
||||||
this.fontSize.set(playlistSongSetting.zoom);
|
this.fontSize.set(gZoom);
|
||||||
this.portraitFontSize = playlistSongSetting.zoom;
|
this.portraitFontSize = gZoom;
|
||||||
} else {
|
|
||||||
this.fontSize.set(1.0);
|
|
||||||
this.portraitFontSize = 1.0;
|
|
||||||
}
|
|
||||||
} else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) {
|
} else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) {
|
||||||
const settings = this.comunitaService.comunitaCantiSettings();
|
const settings = this.comunitaService.comunitaCantiSettings();
|
||||||
const songSetting = settings.find(s => s.id_canti === c.id_canti || s.id_canti === Number(c.id));
|
const songSetting = settings.find(s => s.id_canti === c.id_canti || s.id_canti === Number(c.id));
|
||||||
@@ -315,13 +349,15 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.transposeAmount.set(0);
|
this.transposeAmount.set(0);
|
||||||
this.autoscrollSpeed.set(2);
|
this.autoscrollSpeed.set(2);
|
||||||
}
|
}
|
||||||
this.fontSize.set(1.0);
|
const gZoom = this.settingsService.globalZoom();
|
||||||
this.portraitFontSize = 1.0;
|
this.fontSize.set(gZoom);
|
||||||
|
this.portraitFontSize = gZoom;
|
||||||
} else {
|
} else {
|
||||||
this.transposeAmount.set(0);
|
this.transposeAmount.set(0);
|
||||||
this.autoscrollSpeed.set(2);
|
this.autoscrollSpeed.set(2);
|
||||||
this.fontSize.set(1.0);
|
const gZoom = this.settingsService.globalZoom();
|
||||||
this.portraitFontSize = 1.0;
|
this.fontSize.set(gZoom);
|
||||||
|
this.portraitFontSize = gZoom;
|
||||||
}
|
}
|
||||||
}, { allowSignalWrites: true });
|
}, { allowSignalWrites: true });
|
||||||
|
|
||||||
@@ -344,6 +380,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
onResize(event: any) {
|
onResize(event: any) {
|
||||||
this.windowLandscape.set(window.innerWidth > window.innerHeight);
|
this.windowLandscape.set(window.innerWidth > window.innerHeight);
|
||||||
this.checkLandscapeZoom();
|
this.checkLandscapeZoom();
|
||||||
|
this.checkTitleWrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLandscape(): boolean {
|
private isLandscape(): boolean {
|
||||||
@@ -466,8 +503,26 @@ 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() {
|
ngAfterViewInit() {
|
||||||
this.checkLandscapeZoom();
|
this.checkLandscapeZoom();
|
||||||
|
this.checkTitleWrap();
|
||||||
|
if (this.settingsService.cameraNavigationActive()) {
|
||||||
|
this.initCameraTracking();
|
||||||
|
}
|
||||||
const gestureX = this.gestureCtrl.create({
|
const gestureX = this.gestureCtrl.create({
|
||||||
el: this.el.nativeElement,
|
el: this.el.nativeElement,
|
||||||
direction: 'x',
|
direction: 'x',
|
||||||
@@ -553,6 +608,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fontSize.set(limited);
|
this.fontSize.set(limited);
|
||||||
if (!this.isLandscape()) {
|
if (!this.isLandscape()) {
|
||||||
this.portraitFontSize = limited;
|
this.portraitFontSize = limited;
|
||||||
|
this.settingsService.globalZoom.set(limited);
|
||||||
}
|
}
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
}
|
}
|
||||||
@@ -591,8 +647,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
activePlaylistId,
|
activePlaylistId,
|
||||||
c.id,
|
c.id,
|
||||||
this.transposeAmount(),
|
this.transposeAmount(),
|
||||||
this.autoscrollSpeed(),
|
this.autoscrollSpeed()
|
||||||
this.fontSize()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -620,6 +675,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fontSize.set(limited);
|
this.fontSize.set(limited);
|
||||||
if (!this.isLandscape()) {
|
if (!this.isLandscape()) {
|
||||||
this.portraitFontSize = limited;
|
this.portraitFontSize = limited;
|
||||||
|
this.settingsService.globalZoom.set(limited);
|
||||||
}
|
}
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
this.updatePlaylistSettings();
|
this.updatePlaylistSettings();
|
||||||
@@ -634,6 +690,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fontSize.set(target);
|
this.fontSize.set(target);
|
||||||
if (!this.isLandscape()) {
|
if (!this.isLandscape()) {
|
||||||
this.portraitFontSize = target;
|
this.portraitFontSize = target;
|
||||||
|
this.settingsService.globalZoom.set(target);
|
||||||
}
|
}
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
this.updatePlaylistSettings();
|
this.updatePlaylistSettings();
|
||||||
@@ -662,6 +719,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.youtubePlayerService.seekTo(event.detail.value);
|
this.youtubePlayerService.seekTo(event.detail.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goHome() {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
|
||||||
restart() {
|
restart() {
|
||||||
this.currentLineIndex.set(0);
|
this.currentLineIndex.set(0);
|
||||||
this.youtubePlayerService.seekTo(0);
|
this.youtubePlayerService.seekTo(0);
|
||||||
@@ -919,12 +980,32 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSong() {
|
private getSequenceList(): string[] {
|
||||||
let list = this.playlistService.activeListIds();
|
const currentId = this.canto()?.id;
|
||||||
if (list.length === 0) {
|
|
||||||
list = this.cantiService.canti().map(c => c.id);
|
// 1. Homepage filtered list (includes current sorting like Top Ten, Suggeriti, and any active playlist + additional filters)
|
||||||
|
const filtered = this.playlistService.filteredListIds();
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
if (!currentId || filtered.includes(currentId)) {
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Active playlist raw list if present and contains current song
|
||||||
|
const active = this.playlistService.activeListIds();
|
||||||
|
if (active.length > 0) {
|
||||||
|
if (!currentId || active.includes(currentId)) {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Default fallback: all canti
|
||||||
|
return this.cantiService.canti().map(c => c.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSong() {
|
||||||
|
const list = this.getSequenceList();
|
||||||
|
|
||||||
const currentId = this.canto()?.id;
|
const currentId = this.canto()?.id;
|
||||||
if (!currentId) return;
|
if (!currentId) return;
|
||||||
|
|
||||||
@@ -934,17 +1015,14 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.isLandscapeActive()) {
|
if (this.isLandscapeActive()) {
|
||||||
this.isBlackScreen.set(true);
|
this.isBlackScreen.set(true);
|
||||||
}
|
}
|
||||||
this.router.navigate(['/player'], { queryParams: { id: nextId } });
|
this.router.navigate(['/player'], { queryParams: { id: nextId }, replaceUrl: true });
|
||||||
} else {
|
} else {
|
||||||
this.playlistService.autoPlayPlaylist.set(false);
|
this.playlistService.autoPlayPlaylist.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prevSong() {
|
prevSong() {
|
||||||
let list = this.playlistService.activeListIds();
|
const list = this.getSequenceList();
|
||||||
if (list.length === 0) {
|
|
||||||
list = this.cantiService.canti().map(c => c.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentId = this.canto()?.id;
|
const currentId = this.canto()?.id;
|
||||||
if (!currentId) return;
|
if (!currentId) return;
|
||||||
@@ -955,7 +1033,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.isLandscapeActive()) {
|
if (this.isLandscapeActive()) {
|
||||||
this.isBlackScreen.set(true);
|
this.isBlackScreen.set(true);
|
||||||
}
|
}
|
||||||
this.router.navigate(['/player'], { queryParams: { id: prevId } });
|
this.router.navigate(['/player'], { queryParams: { id: prevId }, replaceUrl: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,7 +1041,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: { id },
|
queryParams: { id },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge',
|
||||||
|
replaceUrl: true
|
||||||
});
|
});
|
||||||
this.restart();
|
this.restart();
|
||||||
}
|
}
|
||||||
@@ -1054,6 +1133,27 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteMyCanto() {
|
||||||
|
const c = this.canto();
|
||||||
|
if (!c || !c.id.startsWith('my_')) return;
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Elimina Canto',
|
||||||
|
message: 'Sei sicuro di voler eliminare questo canto dai tuoi brani personali?',
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Annulla', role: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Elimina',
|
||||||
|
role: 'destructive',
|
||||||
|
handler: () => {
|
||||||
|
this.myCantiService.deleteCanto(c.id);
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
async shareCanto() {
|
async shareCanto() {
|
||||||
const c = this.canto();
|
const c = this.canto();
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
@@ -1135,6 +1235,35 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async printCanto() {
|
||||||
|
const c = this.canto();
|
||||||
|
if (!c) return;
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Esporta in PDF',
|
||||||
|
message: 'Vuoi includere gli accordi nel PDF o esportare solo il testo?',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Solo Testo',
|
||||||
|
handler: () => {
|
||||||
|
this.playlistService.printPlaylist(c.titolo, [c], false, { [c.id || c.id_canti]: this.transposeAmount() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Testo e Accordi',
|
||||||
|
handler: () => {
|
||||||
|
this.playlistService.printPlaylist(c.titolo, [c], true, { [c.id || c.id_canti]: this.transposeAmount() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
private initPlayer(id: string) {
|
private initPlayer(id: string) {
|
||||||
if (!this.youtubePlayerService.isPlayerSupported()) {
|
if (!this.youtubePlayerService.isPlayerSupported()) {
|
||||||
return;
|
return;
|
||||||
@@ -1241,7 +1370,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleCameraNavigation() {
|
toggleCameraNavigation() {
|
||||||
if (this.enableCameraNavigation()) {
|
if (this.settingsService.cameraNavigationActive()) {
|
||||||
this.stopCameraNavigation();
|
this.stopCameraNavigation();
|
||||||
} else {
|
} else {
|
||||||
this.startCameraNavigation();
|
this.startCameraNavigation();
|
||||||
@@ -1249,7 +1378,12 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startCameraNavigation() {
|
startCameraNavigation() {
|
||||||
this.enableCameraNavigation.set(true);
|
this.settingsService.cameraNavigationActive.set(true);
|
||||||
|
this.initCameraTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
initCameraTracking() {
|
||||||
|
if (!this.settingsService.cameraNavigationActive()) return;
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const videoEl = document.querySelector('#face-preview-video') as HTMLVideoElement;
|
const videoEl = document.querySelector('#face-preview-video') as HTMLVideoElement;
|
||||||
@@ -1263,16 +1397,17 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.prev(true);
|
this.prev(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
this.enableCameraNavigation.set(false);
|
this.settingsService.cameraNavigationActive.set(false);
|
||||||
alert('Impossibile accedere alla fotocamera. Assicurati di aver concesso i permessi e di usare HTTPS.');
|
const errorMsg = e?.message || e?.name || (typeof e === 'object' ? JSON.stringify(e) : String(e)) || 'Errore sconosciuto';
|
||||||
|
alert(`Impossibile accedere alla fotocamera. Assicurati di aver concesso i permessi e di usare HTTPS.\n\nDettagli errore: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopCameraNavigation() {
|
stopCameraNavigation() {
|
||||||
this.enableCameraNavigation.set(false);
|
this.settingsService.cameraNavigationActive.set(false);
|
||||||
this.faceDetector.stop();
|
this.faceDetector.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1280,7 +1415,11 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.exitFullscreen();
|
this.exitFullscreen();
|
||||||
this.stopAutoscroll();
|
this.stopAutoscroll();
|
||||||
this.logPreviousSongTime();
|
this.logPreviousSongTime();
|
||||||
this.stopCameraNavigation();
|
if (this.settingsService.cameraNavigationActive()) {
|
||||||
|
this.faceDetector.pause();
|
||||||
|
} else {
|
||||||
|
this.faceDetector.stop();
|
||||||
|
}
|
||||||
this.channel.close();
|
this.channel.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title class="outfit-font">Gestione Playlist</ion-title>
|
<ion-title class="outfit-font">Gestione Playlist</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
|
<ion-button (click)="exportPlaylistPdf()" [disabled]="localSongs.length === 0" title="Esporta PDF">
|
||||||
|
<ion-icon slot="icon-only" name="print-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
<ion-button (click)="savePlaylist()" [disabled]="localSongs.length === 0">
|
<ion-button (click)="savePlaylist()" [disabled]="localSongs.length === 0">
|
||||||
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -14,8 +17,12 @@
|
|||||||
|
|
||||||
<ion-content class="bg-gradient">
|
<ion-content class="bg-gradient">
|
||||||
<div class="ion-padding">
|
<div class="ion-padding">
|
||||||
<div class="header-info glass ion-margin-bottom" *ngIf="localSongs.length > 0">
|
<div class="header-info glass ion-margin-bottom" *ngIf="localSongs.length > 0" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<p>Trascina i canti per riordinarli. Una volta finito puoi salvare la playlist.</p>
|
<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>
|
||||||
|
|
||||||
<div cdkDropList class="song-list" (cdkDropListDropped)="drop($event)">
|
<div cdkDropList class="song-list" (cdkDropListDropped)="drop($event)">
|
||||||
|
|||||||
@@ -51,6 +51,42 @@ export class PlaylistPage {
|
|||||||
public qrCodeImage: string | null = null;
|
public qrCodeImage: string | null = null;
|
||||||
public savedPlaylistName: 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() {
|
constructor() {
|
||||||
// Initial copy to allow local reordering
|
// Initial copy to allow local reordering
|
||||||
this.localSongs = [...this.selectedSongs()];
|
this.localSongs = [...this.selectedSongs()];
|
||||||
@@ -236,4 +272,42 @@ export class PlaylistPage {
|
|||||||
const index = this.myCantiService.myCanti().findIndex(c => c.id === song.id);
|
const index = this.myCantiService.myCanti().findIndex(c => c.id === song.id);
|
||||||
return index !== -1 ? index + 1 : 0;
|
return index !== -1 ? index + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportPlaylistPdf() {
|
||||||
|
if (this.localSongs.length === 0) return;
|
||||||
|
|
||||||
|
const songSettings = this.getPlaylistSongSettings() || {};
|
||||||
|
const transpositions: { [songId: string]: number } = {};
|
||||||
|
for (const key of Object.keys(songSettings)) {
|
||||||
|
if (songSettings[key] && songSettings[key].tonalita !== undefined) {
|
||||||
|
transpositions[key] = songSettings[key].tonalita;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Esporta in PDF',
|
||||||
|
message: 'Vuoi includere gli accordi nel PDF o esportare solo il testo?',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Solo Testo',
|
||||||
|
handler: () => {
|
||||||
|
const name = this.savedPlaylistName || 'Playlist';
|
||||||
|
this.playlistService.printPlaylist(name, this.localSongs, false, transpositions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Testo e Accordi',
|
||||||
|
handler: () => {
|
||||||
|
const name = this.savedPlaylistName || 'Playlist';
|
||||||
|
this.playlistService.printPlaylist(name, this.localSongs, true, transpositions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,18 @@
|
|||||||
<ion-input [(ngModel)]="title" placeholder="Es: Il Signore è la mia salvezza"></ion-input>
|
<ion-input [(ngModel)]="title" placeholder="Es: Il Signore è la mia salvezza"></ion-input>
|
||||||
</ion-item>
|
</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">
|
<div class="category-selectors">
|
||||||
<ion-item class="custom-input-item select-item">
|
<ion-item class="custom-input-item select-item">
|
||||||
<ion-label position="stacked">Momento Liturgico</ion-label>
|
<ion-label position="stacked">Momento Liturgico</ion-label>
|
||||||
@@ -73,9 +85,14 @@
|
|||||||
<!-- TOOLBARS -->
|
<!-- TOOLBARS -->
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<div class="horizontal-toolbar">
|
<div class="horizontal-toolbar">
|
||||||
<ion-button *ngFor="let tag of commonTags" size="small" fill="outline" (click)="insertText(tag.start)">
|
<ng-container *ngFor="let tag of commonTags">
|
||||||
{{ tag.label }}
|
<ion-button size="small" fill="outline" (click)="insertText(tag.start)">
|
||||||
</ion-button>
|
{{ tag.label }}
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="tag.end" size="small" fill="outline" color="medium" (click)="insertText(tag.end)">
|
||||||
|
Fine {{ tag.label }}
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,12 +126,28 @@
|
|||||||
<!-- EDITOR AREA -->
|
<!-- EDITOR AREA -->
|
||||||
<div class="editor-wrapper" [class.hc]="isHighContrast">
|
<div class="editor-wrapper" [class.hc]="isHighContrast">
|
||||||
<div class="editor-header">
|
<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>
|
<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>
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0" title="Annulla ultima modifica">
|
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0" title="Annulla ultima modifica">
|
||||||
<ion-icon name="undo-outline"></ion-icon>
|
<ion-icon name="arrow-undo-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" size="small" (click)="deduceChords()" [disabled]="!content" title="Deduci accordi per le altre strofe">
|
<ion-button fill="clear" size="small" (click)="deduceChords()" [disabled]="!content" title="Deduci accordi per le altre strofe">
|
||||||
<ion-icon name="musical-notes-outline"></ion-icon>
|
<ion-icon name="musical-notes-outline"></ion-icon>
|
||||||
@@ -125,25 +158,58 @@
|
|||||||
<ion-button fill="clear" size="small" (click)="chooseFile()" title="Allega file o PDF">
|
<ion-button fill="clear" size="small" (click)="chooseFile()" title="Allega file o PDF">
|
||||||
<ion-icon name="document-attach-outline"></ion-icon>
|
<ion-icon name="document-attach-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<ion-button fill="clear" size="small" (click)="toggleRawPasteMode()" [color]="isRawPasteModeActive ? 'warning' : 'medium'" [title]="isRawPasteModeActive ? 'Incolla Raw: Attivo (il testo incollato non verrà filtrato)' : 'Attiva Incolla Raw (incolla senza filtri)'" style="font-size: 11px; font-weight: 800; font-family: 'Outfit', sans-serif;">
|
||||||
|
RAW
|
||||||
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ion-item class="custom-input-item textarea-item">
|
<div class="editor-textarea-container">
|
||||||
<ion-textarea
|
<!-- Input textarea -->
|
||||||
#contentTextarea
|
<textarea
|
||||||
|
#nativeTextarea
|
||||||
[(ngModel)]="content"
|
[(ngModel)]="content"
|
||||||
placeholder="Scrivi o scansiona..."
|
placeholder="Scrivi o scansiona..."
|
||||||
rows="18"
|
(paste)="onPaste($event)"
|
||||||
class="content-textarea"
|
spellcheck="false"
|
||||||
(paste)="onPaste($event)">
|
autocapitalize="none"
|
||||||
</ion-textarea>
|
autocomplete="off"
|
||||||
</ion-item>
|
autocorrect="off">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-item class="custom-input-item">
|
<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)]="author" placeholder="Autore"></ion-input>
|
||||||
<ion-input [(ngModel)]="youtubeLink" placeholder="URL YouTube"></ion-input>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- 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>
|
</ion-list>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
@@ -159,7 +225,8 @@
|
|||||||
<div class="preview-card" [class.hc]="isHighContrast">
|
<div class="preview-card" [class.hc]="isHighContrast">
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<span class="preview-title">Anteprima Attiva</span>
|
<span class="preview-title">Anteprima Attiva</span>
|
||||||
<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>
|
<ion-icon slot="start" [name]="showChordsPreview ? 'musical-notes-outline' : 'text-outline'"></ion-icon>
|
||||||
{{ showChordsPreview ? 'Con Accordi' : 'Solo Testo' }}
|
{{ showChordsPreview ? 'Con Accordi' : 'Solo Testo' }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -170,26 +237,28 @@
|
|||||||
<p class="preview-song-author" *ngIf="author">{{ author }}</p>
|
<p class="preview-song-author" *ngIf="author">{{ author }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-lyrics-container">
|
<div class="preview-lyrics-container">
|
||||||
<div *ngFor="let section of parsedSections"
|
<div class="lyrics-view">
|
||||||
class="preview-section"
|
<div *ngFor="let section of parsedSections"
|
||||||
[class.chorus]="section.type === 'chorus'"
|
class="section"
|
||||||
[class.verse-num]="section.type === 'verse_num'">
|
[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 === 'chorus'" class="section-label">Rit.</div>
|
||||||
<div *ngIf="section.type === 'verse_num' && section.verseNumber" class="preview-section-label preview-verse-num-label">{{ section.verseNumber }}.</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="preview-lyric-line">
|
<div *ngFor="let line of section.lines" class="lyric-line">
|
||||||
<!-- Chord mode -->
|
<!-- Chord mode -->
|
||||||
<ng-container *ngIf="showChordsPreview; else textOnly">
|
<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 *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="preview-chord">{{ seg.chord }}</span>
|
<span *ngIf="seg.chord" class="chord">{{ seg.chord }}</span>
|
||||||
<span class="preview-seg-text">{{ seg.text }}</span>
|
<span class="seg-text">{{ seg.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- Text only mode -->
|
<!-- Text only mode -->
|
||||||
<ng-template #textOnly>
|
<ng-template #textOnly>
|
||||||
{{ line.text }}
|
{{ line.text }}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,8 +272,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Hidden inputs -->
|
<!-- Hidden inputs -->
|
||||||
<input type="file" #cameraInput (change)="onFileSelected($event, true)" accept="image/*" capture="camera" style="display: none;">
|
<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;">
|
<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 */
|
/* Textarea inside high contrast must have white background and black text */
|
||||||
.content-textarea {
|
.editor-textarea-container {
|
||||||
--color: #000000 !important;
|
|
||||||
color: #000000 !important;
|
|
||||||
background: #ffffff !important;
|
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 */
|
/* 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 {
|
.editor-textarea-container {
|
||||||
--color: #000000 !important;
|
background: #ffffff;
|
||||||
color: #000000 !important;
|
|
||||||
background: #ffffff !important;
|
textarea {
|
||||||
--background: #ffffff !important;
|
color: #000000;
|
||||||
font-size: 18px !important;
|
caret-color: #000000;
|
||||||
font-weight: 700 !important;
|
}
|
||||||
caret-color: #000000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep {
|
:host ::ng-deep {
|
||||||
.content-textarea {
|
.editor-textarea-container {
|
||||||
font-family: 'Courier New', Courier, monospace;
|
position: relative;
|
||||||
font-size: 15px;
|
width: 100%;
|
||||||
font-weight: 600;
|
height: 450px;
|
||||||
--color: #ffffff !important;
|
background: #111111;
|
||||||
color: #ffffff !important;
|
border-radius: 0 0 14px 14px;
|
||||||
--padding-start: 16px;
|
overflow: hidden;
|
||||||
--padding-end: 16px;
|
box-sizing: border-box;
|
||||||
--padding-top: 16px;
|
|
||||||
--padding-bottom: 16px;
|
textarea {
|
||||||
min-height: 400px;
|
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 {
|
.category-selectors {
|
||||||
@@ -569,8 +591,14 @@ body.high-contrast :host ::ng-deep {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-section {
|
.lyrics-view {
|
||||||
margin-bottom: 1.8rem;
|
max-width: 900px;
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.chorus {
|
&.chorus {
|
||||||
@@ -581,25 +609,30 @@ body.high-contrast :host ::ng-deep {
|
|||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-section-label {
|
.section-label {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.5rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-lyric-line {
|
.lyric-line {
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: all 0.3s ease;
|
||||||
min-height: 1.5em;
|
min-height: 1.5em;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
letter-spacing: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-chord-segment {
|
.chord-segment {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
@@ -609,16 +642,16 @@ body.high-contrast :host ::ng-deep {
|
|||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-chord {
|
.chord {
|
||||||
font-size: 0.78em;
|
font-size: 0.75em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ion-color-secondary);
|
color: var(--ion-color-secondary);
|
||||||
height: 1.25em;
|
height: 1.2em;
|
||||||
margin-bottom: -0.2em;
|
margin-bottom: -0.2em;
|
||||||
padding-right: 0.25em;
|
padding-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-seg-text {
|
.seg-text {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
&::after {
|
&::after {
|
||||||
content: '\200b';
|
content: '\200b';
|
||||||
@@ -654,6 +687,16 @@ body.high-contrast :host ::ng-deep {
|
|||||||
--color: #000000;
|
--color: #000000;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-transpose-group {
|
||||||
|
border-color: #000000 !important;
|
||||||
|
span {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
ion-button {
|
||||||
|
--color: #000000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-body {
|
.preview-body {
|
||||||
@@ -672,23 +715,23 @@ body.high-contrast :host ::ng-deep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-lyrics-container {
|
.preview-lyrics-container {
|
||||||
.preview-section {
|
.section {
|
||||||
&.chorus {
|
&.chorus {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-left: 3px solid #000000;
|
border-left: 3px solid #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-section-label {
|
.section-label {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-lyric-line {
|
.lyric-line {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-chord-segment {
|
.chord-segment {
|
||||||
.preview-chord {
|
.chord {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -703,6 +746,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 STYLES FOR INTUITIVE MOBILE RESPONSIVENESS */
|
||||||
.custom-mobile-segment {
|
.custom-mobile-segment {
|
||||||
display: none;
|
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 { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { IonicModule, ToastController, IonTextarea, PopoverController, NavController, AlertController } from '@ionic/angular';
|
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 { ThemeService } from '../../services/theme.service';
|
||||||
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
|
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
|
||||||
import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service';
|
import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service';
|
||||||
|
import { YoutubePlayerService } from '../../services/youtube-player.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-propose-canto',
|
selector: 'app-propose-canto',
|
||||||
@@ -17,8 +18,9 @@ import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, IonicModule, RouterModule]
|
imports: [CommonModule, FormsModule, IonicModule, RouterModule]
|
||||||
})
|
})
|
||||||
export class ProposeCantoPage implements OnInit {
|
export class ProposeCantoPage implements OnInit, OnDestroy {
|
||||||
@ViewChild('contentTextarea', { static: false }) contentTextarea!: IonTextarea;
|
@ViewChild('nativeTextarea', { static: false }) nativeTextarea!: ElementRef<HTMLTextAreaElement>;
|
||||||
|
|
||||||
@ViewChild('cameraInput', { static: false }) cameraInput!: ElementRef;
|
@ViewChild('cameraInput', { static: false }) cameraInput!: ElementRef;
|
||||||
@ViewChild('fileInput', { static: false }) fileInput!: ElementRef;
|
@ViewChild('fileInput', { static: false }) fileInput!: ElementRef;
|
||||||
|
|
||||||
@@ -31,12 +33,155 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
public lyricsParser = inject(LyricsParserService);
|
public lyricsParser = inject(LyricsParserService);
|
||||||
private alertCtrl = inject(AlertController);
|
private alertCtrl = inject(AlertController);
|
||||||
|
public youtubePlayerService = inject(YoutubePlayerService);
|
||||||
|
|
||||||
showChordsPreview: boolean = true;
|
showChordsPreview: boolean = true;
|
||||||
activeTab: string = 'editor';
|
activeTab: string = 'editor';
|
||||||
|
transposeAmount: number = 0;
|
||||||
|
isRawPasteModeActive: boolean = false;
|
||||||
|
|
||||||
|
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[] {
|
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() {
|
toggleChordsPreview() {
|
||||||
@@ -48,6 +193,8 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
youtubeLink: string = '';
|
youtubeLink: string = '';
|
||||||
selectedLiturgico: number[] = [];
|
selectedLiturgico: number[] = [];
|
||||||
selectedTematico: number[] = [];
|
selectedTematico: number[] = [];
|
||||||
|
durata: string = '';
|
||||||
|
bpm: number | null = null;
|
||||||
|
|
||||||
get sortedLiturgico() {
|
get sortedLiturgico() {
|
||||||
return [...this.cantiService.indiceLiturgico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
|
return [...this.cantiService.indiceLiturgico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
|
||||||
@@ -77,31 +224,31 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
groupedChords = [
|
groupedChords = [
|
||||||
{
|
{
|
||||||
root: 'DO',
|
root: 'DO',
|
||||||
chords: ['DO', 'DO-', 'DO#', 'DO#-', 'DO7', 'DO-7', 'DOmaj7', 'DO4', 'DOdim', 'DOm7']
|
chords: ['DO', 'DO-', 'DO#', 'DO#-', 'DO7', 'DO-7', 'DO4']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: 'RE',
|
root: 'RE',
|
||||||
chords: ['RE', 'RE-', 'RE#', 'RE#-', 'RE7', 'RE-7', 'REmaj7', 'RE4', 'REdim', 'REm7']
|
chords: ['RE', 'RE-', 'RE#', 'RE#-', 'RE7', 'RE-7', 'RE4']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: 'MI',
|
root: 'MI',
|
||||||
chords: ['MI', 'MI-', 'MI7', 'MI-7', 'MImaj7', 'MI4', 'MIdim']
|
chords: ['MI', 'MI-', 'MI7', 'MI-7', 'MI4']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: 'FA',
|
root: 'FA',
|
||||||
chords: ['FA', 'FA-', 'FA#', 'FA#-', 'FA7', 'FAmaj7', 'FA4', 'FAdim', 'FAm7']
|
chords: ['FA', 'FA-', 'FA#', 'FA#-', 'FA7', 'FA-7', 'FA4']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: 'SOL',
|
root: 'SOL',
|
||||||
chords: ['SOL', 'SOL-', 'SOL#', 'SOL#-', 'SOL7', 'SOLmaj7', 'SOL4', 'SOLdim', 'SOLm7']
|
chords: ['SOL', 'SOL-', 'SOL#', 'SOL#-', 'SOL7', 'SOL-7', 'SOL4']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: 'LA',
|
root: 'LA',
|
||||||
chords: ['LA', 'LA-', 'LA#', 'LA#-', 'LA7', 'LA-7', 'LAmaj7', 'LA4', 'LAdim', 'LAm7']
|
chords: ['LA', 'LA-', 'LA#', 'LA#-', 'LA7', 'LA-7', 'LA4']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: 'SI',
|
root: 'SI',
|
||||||
chords: ['SI', 'SI-', 'SI7', 'SI-7', 'SImaj7', 'SI4', 'SIdim']
|
chords: ['SI', 'SI-', 'SI7', 'SI-7', 'SI4']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -152,6 +299,8 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
this.author = song.autore || '';
|
this.author = song.autore || '';
|
||||||
this.youtubeLink = song.link_youtube || '';
|
this.youtubeLink = song.link_youtube || '';
|
||||||
this.content = song.accordi || song.testo || '';
|
this.content = song.accordi || song.testo || '';
|
||||||
|
this.durata = song.durata || '';
|
||||||
|
this.bpm = song.bpm !== undefined ? song.bpm : null;
|
||||||
|
|
||||||
// Pre-populate liturgico and tematico lists
|
// Pre-populate liturgico and tematico lists
|
||||||
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
|
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
|
||||||
@@ -164,8 +313,12 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.youtubePlayerService.stop();
|
||||||
|
}
|
||||||
|
|
||||||
async insertText(tag: string) {
|
async insertText(tag: string) {
|
||||||
const input = await this.contentTextarea.getInputElement();
|
const input = this.nativeTextarea.nativeElement;
|
||||||
const start = input.selectionStart || 0;
|
const start = input.selectionStart || 0;
|
||||||
const end = input.selectionEnd || 0;
|
const end = input.selectionEnd || 0;
|
||||||
|
|
||||||
@@ -181,6 +334,19 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
this.insertText(`[${chord}]`);
|
this.insertText(`[${chord}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleRawPasteMode() {
|
||||||
|
this.isRawPasteModeActive = !this.isRawPasteModeActive;
|
||||||
|
|
||||||
|
// Show a toast indicating whether the Raw Paste mode has been activated or deactivated
|
||||||
|
this.toastController.create({
|
||||||
|
message: this.isRawPasteModeActive
|
||||||
|
? 'Modalità Incolla Raw ATTIVA: incolla liberamente senza filtri'
|
||||||
|
: 'Modalità Incolla Raw DISATTIVA: i filtri automatici sono attivi',
|
||||||
|
duration: 2500,
|
||||||
|
color: this.isRawPasteModeActive ? 'warning' : 'primary'
|
||||||
|
}).then(toast => toast.present());
|
||||||
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
if (this.undoStack.length > 0) {
|
if (this.undoStack.length > 0) {
|
||||||
const previous = this.undoStack.pop();
|
const previous = this.undoStack.pop();
|
||||||
@@ -441,6 +607,10 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onPaste(event: ClipboardEvent) {
|
async onPaste(event: ClipboardEvent) {
|
||||||
|
if (this.isRawPasteModeActive) {
|
||||||
|
// In raw paste mode, we let the browser handle paste natively with no processing
|
||||||
|
return;
|
||||||
|
}
|
||||||
const items = event.clipboardData?.items;
|
const items = event.clipboardData?.items;
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
|
|
||||||
@@ -1554,7 +1724,9 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
link_youtube: this.youtubeLink,
|
link_youtube: this.youtubeLink,
|
||||||
testo: this.content,
|
testo: this.content,
|
||||||
accordi: 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
|
// 2. Clone/convert remote playlist to local personal playlist
|
||||||
@@ -1587,7 +1759,9 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
link_youtube: this.youtubeLink,
|
link_youtube: this.youtubeLink,
|
||||||
testo: this.content,
|
testo: this.content,
|
||||||
accordi: this.content, // Save to both fields for compatibility
|
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) {
|
if (this.editId && !this.editId.startsWith('my_') && savedCanto && savedCanto.id) {
|
||||||
|
|||||||
@@ -118,6 +118,14 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-toggle slot="end" [checked]="settingsService.showUpdateDate()" (ionChange)="settingsService.toggleShowUpdateDate()" color="secondary"></ion-toggle>
|
<ion-toggle slot="end" [checked]="settingsService.showUpdateDate()" (ionChange)="settingsService.toggleShowUpdateDate()" color="secondary"></ion-toggle>
|
||||||
</ion-item>
|
</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-item class="transparent-item" lines="none">
|
||||||
<ion-icon name="swap-vertical-outline" slot="start" color="secondary"></ion-icon>
|
<ion-icon name="swap-vertical-outline" slot="start" color="secondary"></ion-icon>
|
||||||
<ion-label class="outfit-font">
|
<ion-label class="outfit-font">
|
||||||
@@ -273,6 +281,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Invio Dati Statistici -->
|
||||||
<div class="settings-group glass ion-margin-bottom">
|
<div class="settings-group glass ion-margin-bottom">
|
||||||
<ion-item class="transparent-item" lines="none">
|
<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() {
|
async manualRestore() {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Ripristina con Codice',
|
header: 'Ripristina con Codice',
|
||||||
@@ -159,7 +172,9 @@ export class SettingsPage {
|
|||||||
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
||||||
autore: item.autore || '',
|
autore: item.autore || '',
|
||||||
link_youtube: item.link_youtube || '',
|
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) {
|
if (customSongs.length > 0) {
|
||||||
@@ -347,7 +362,9 @@ export class SettingsPage {
|
|||||||
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
||||||
autore: item.autore || '',
|
autore: item.autore || '',
|
||||||
link_youtube: item.link_youtube || '',
|
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);
|
this.myCantiService.myCanti.set(customSongs);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Injectable, signal, inject, effect } from '@angular/core';
|
import { Injectable, signal, inject, effect } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Storage } from '@ionic/storage-angular';
|
import { Storage } from '@ionic/storage-angular';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
id_canto?: number;
|
id_canto?: number;
|
||||||
@@ -127,7 +128,18 @@ export class CantiLettureService {
|
|||||||
|
|
||||||
const savedDate = localStorage.getItem('selected-mass-date');
|
const savedDate = localStorage.getItem('selected-mass-date');
|
||||||
if (savedDate) {
|
if (savedDate) {
|
||||||
this.selectedMassDate.set(savedDate);
|
const masses = this.availableMasses();
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const todayStr = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
// Only restore savedDate if it exists in available masses and is not outdated
|
||||||
|
const isValidAndNotPast = masses.some(m => m.date === savedDate && m.date >= todayStr);
|
||||||
|
if (isValidAndNotPast) {
|
||||||
|
this.selectedMassDate.set(savedDate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch fresh data
|
// Fetch fresh data
|
||||||
@@ -137,24 +149,45 @@ export class CantiLettureService {
|
|||||||
async fetchData() {
|
async fetchData() {
|
||||||
try {
|
try {
|
||||||
let fetchedData: CantiLettureData | null = null;
|
let fetchedData: CantiLettureData | null = null;
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const todayStr = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
const isProduction = window.location.hostname.includes('canticristiani.it');
|
const isProduction = window.location.hostname.includes('canticristiani.it');
|
||||||
const primaryUrl = isProduction
|
const primaryUrl = isProduction
|
||||||
? `${window.location.origin}${this.SECURE_JSON_URL}`
|
? `${window.location.origin}${this.SECURE_JSON_URL}`
|
||||||
: 'https://www.canticristiani.it/api/cantiletture.json';
|
: 'https://www.canticristiani.it/api/cantiletture.json';
|
||||||
const fallbackUrl = isProduction
|
|
||||||
? 'https://www.canticristiani.it/api/cantiletture.json'
|
|
||||||
: this.JSON_URL;
|
|
||||||
|
|
||||||
|
// 1. Try static JSON endpoint first
|
||||||
try {
|
try {
|
||||||
console.log('Fetching mass data from primary URL:', primaryUrl);
|
console.log('Fetching mass data from primary URL:', primaryUrl);
|
||||||
fetchedData = await firstValueFrom(this.http.get<CantiLettureData>(`${primaryUrl}?t=${Date.now()}`));
|
const res = await firstValueFrom(this.http.get<CantiLettureData>(`${primaryUrl}?t=${Date.now()}`));
|
||||||
|
if (res && res.masses && res.week_end && res.week_end >= todayStr) {
|
||||||
|
fetchedData = res;
|
||||||
|
} else {
|
||||||
|
console.warn('Primary JSON data is missing or out of date:', res?.week_end);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Primary fetch failed, trying fallback URL...', fallbackUrl, err);
|
console.warn('Primary fetch failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to direct API endpoint using Basic Auth credentials
|
||||||
|
if (!fetchedData) {
|
||||||
try {
|
try {
|
||||||
fetchedData = await firstValueFrom(this.http.get<CantiLettureData>(`${fallbackUrl}?t=${Date.now()}`));
|
console.log('Fetching mass data from direct API:', this.JSON_URL);
|
||||||
} catch (fallbackErr) {
|
const authUser = environment.apiAuthUser || 'canti';
|
||||||
console.error('Fallback fetch also failed:', fallbackErr);
|
const authPass = environment.apiAuthPass || 'antani2026';
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'Authorization': 'Basic ' + btoa(`${authUser}:${authPass}`)
|
||||||
|
});
|
||||||
|
const res = await firstValueFrom(this.http.get<CantiLettureData>(`${this.JSON_URL}?t=${Date.now()}`, { headers }));
|
||||||
|
if (res && res.masses) {
|
||||||
|
fetchedData = res;
|
||||||
|
}
|
||||||
|
} catch (authErr) {
|
||||||
|
console.error('Direct API fetch failed:', authErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,24 +227,29 @@ export class CantiLettureService {
|
|||||||
|
|
||||||
this.availableMasses.set(massesList);
|
this.availableMasses.set(massesList);
|
||||||
|
|
||||||
// Always select today's mass if available on load, else find closest future date
|
|
||||||
if (massesList.length > 0) {
|
if (massesList.length > 0) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const year = now.getFullYear();
|
const year = now.getFullYear();
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(now.getDate()).padStart(2, '0');
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
const todayStr = `${year}-${month}-${day}`;
|
const todayStr = `${year}-${month}-${day}`;
|
||||||
const match = massesList.find(m => m.date === todayStr);
|
|
||||||
if (match) {
|
const currentSelected = this.selectedMassDate();
|
||||||
this.selectedMassDate.set(match.date);
|
const isCurrentValid = currentSelected && massesList.some(m => m.date === currentSelected && m.date >= todayStr);
|
||||||
} else {
|
|
||||||
// If today is not present, find the first date that is in the future
|
if (!isCurrentValid) {
|
||||||
const futureMatch = massesList.find(m => m.date >= todayStr);
|
const match = massesList.find(m => m.date === todayStr);
|
||||||
if (futureMatch) {
|
if (match) {
|
||||||
this.selectedMassDate.set(futureMatch.date);
|
this.selectedMassDate.set(match.date);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise fall back to the last available mass date (closest to today)
|
// If today is not present, find the first date that is in the future
|
||||||
this.selectedMassDate.set(massesList[massesList.length - 1].date);
|
const futureMatch = massesList.find(m => m.date >= todayStr);
|
||||||
|
if (futureMatch) {
|
||||||
|
this.selectedMassDate.set(futureMatch.date);
|
||||||
|
} else {
|
||||||
|
// Otherwise fall back to the last available mass date (closest to today)
|
||||||
|
this.selectedMassDate.set(massesList[massesList.length - 1].date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface Canto {
|
|||||||
data_update?: string;
|
data_update?: string;
|
||||||
nonValidato?: boolean;
|
nonValidato?: boolean;
|
||||||
isPersonal?: boolean;
|
isPersonal?: boolean;
|
||||||
|
durata?: string;
|
||||||
|
bpm?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Indice {
|
export interface Indice {
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ export class ComunitaService {
|
|||||||
id_momenti: [],
|
id_momenti: [],
|
||||||
data_update: cp.data_update || '',
|
data_update: cp.data_update || '',
|
||||||
nonValidato: Number(cp.stato) === 10,
|
nonValidato: Number(cp.stato) === 10,
|
||||||
isPersonal: true
|
isPersonal: true,
|
||||||
|
durata: cp.durata || '',
|
||||||
|
bpm: cp.bpm !== undefined && cp.bpm !== null ? Number(cp.bpm) : undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.comunitaCode.set(trimmedCode);
|
this.comunitaCode.set(trimmedCode);
|
||||||
|
|||||||
@@ -9,111 +9,106 @@ export class FaceDetectorService {
|
|||||||
public isTilted = signal<boolean>(false);
|
public isTilted = signal<boolean>(false);
|
||||||
|
|
||||||
private stream: MediaStream | null = null;
|
private stream: MediaStream | null = null;
|
||||||
private camera: any = null;
|
private animFrameId: number | null = null;
|
||||||
private faceMesh: any = null;
|
private faceMesh: any = null;
|
||||||
private onTiltCallback: ((direction: 'next' | 'prev') => void) | null = null;
|
private onTiltCallback: ((direction: 'next' | 'prev') => void) | null = null;
|
||||||
|
|
||||||
// Gesture state machine
|
// Gesture state machine
|
||||||
private tiltStartTime: number = 0;
|
private tiltStartTime: number = 0;
|
||||||
private inCooldown: boolean = false;
|
private inCooldown: boolean = false;
|
||||||
private readonly TILT_THRESHOLD = 15; // Degrees to trigger next/prev page
|
private lastTriggerTime: number = 0;
|
||||||
|
private readonly TILT_THRESHOLD = 30; // Degrees to trigger next/prev page (30° for stability)
|
||||||
private readonly TILT_HOLD_MS = 300; // How long to hold the tilt
|
private readonly TILT_HOLD_MS = 300; // How long to hold the tilt
|
||||||
private readonly RETURN_THRESHOLD = 6; // Degrees to reset cooldown
|
private readonly RETURN_THRESHOLD = 15; // Degrees to reset cooldown
|
||||||
|
private readonly TRIGGER_COOLDOWN_MS = 3000; // Minimum time between consecutive gestures in ms
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads MediaPipe scripts dynamically if not already loaded.
|
* Loads MediaPipe script dynamically if not already loaded.
|
||||||
*/
|
*/
|
||||||
private loadScripts(): Promise<void> {
|
private loadScripts(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if ((window as any).FaceMesh && (window as any).Camera) {
|
if ((window as any).FaceMesh) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraScript = document.createElement('script');
|
|
||||||
cameraScript.src = 'https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js';
|
|
||||||
cameraScript.crossOrigin = 'anonymous';
|
|
||||||
|
|
||||||
const faceMeshScript = document.createElement('script');
|
const faceMeshScript = document.createElement('script');
|
||||||
faceMeshScript.src = 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js';
|
faceMeshScript.src = 'assets/mediapipe/face_mesh.js';
|
||||||
faceMeshScript.crossOrigin = 'anonymous';
|
faceMeshScript.onload = () => resolve();
|
||||||
|
|
||||||
cameraScript.onload = () => {
|
|
||||||
document.head.appendChild(faceMeshScript);
|
|
||||||
};
|
|
||||||
|
|
||||||
faceMeshScript.onload = () => {
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
cameraScript.onerror = (err) => reject(err);
|
|
||||||
faceMeshScript.onerror = (err) => reject(err);
|
faceMeshScript.onerror = (err) => reject(err);
|
||||||
|
|
||||||
document.head.appendChild(cameraScript);
|
document.head.appendChild(faceMeshScript);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts camera capture and face mesh tracking.
|
* Starts or re-binds camera capture and face mesh tracking.
|
||||||
|
* If a stream is already active (e.g. navigating between songs on iPad), reuses it
|
||||||
|
* to avoid triggering repeated permission prompts on iOS/Safari.
|
||||||
*/
|
*/
|
||||||
async start(videoElement: HTMLVideoElement, onTilt: (direction: 'next' | 'prev') => void): Promise<void> {
|
async start(videoElement: HTMLVideoElement, onTilt: (direction: 'next' | 'prev') => void): Promise<void> {
|
||||||
if (this.isCameraActive()) return;
|
|
||||||
this.onTiltCallback = onTilt;
|
this.onTiltCallback = onTilt;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.loadScripts();
|
await this.loadScripts();
|
||||||
|
|
||||||
// Request camera permissions and stream
|
const isStreamActive = this.stream &&
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
this.stream.active &&
|
||||||
video: {
|
this.stream.getVideoTracks().some(track => track.readyState === 'live');
|
||||||
width: { ideal: 320 },
|
|
||||||
height: { ideal: 240 },
|
if (!isStreamActive) {
|
||||||
facingMode: 'user'
|
// Request camera permissions and stream ONCE
|
||||||
},
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: false
|
video: {
|
||||||
});
|
width: { ideal: 320 },
|
||||||
|
height: { ideal: 240 },
|
||||||
|
facingMode: 'user'
|
||||||
|
},
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
videoElement.srcObject = this.stream;
|
videoElement.srcObject = this.stream;
|
||||||
videoElement.setAttribute('playsinline', 'true');
|
videoElement.setAttribute('playsinline', 'true');
|
||||||
|
(videoElement as any).playsInline = true;
|
||||||
videoElement.muted = true;
|
videoElement.muted = true;
|
||||||
videoElement.play();
|
|
||||||
|
|
||||||
const FaceMeshLib = (window as any).FaceMesh;
|
try {
|
||||||
const CameraLib = (window as any).Camera;
|
await videoElement.play();
|
||||||
|
} catch (playErr) {
|
||||||
if (!FaceMeshLib || !CameraLib) {
|
console.warn('[FaceDetector] Video play warning:', playErr);
|
||||||
throw new Error('MediaPipe libraries failed to initialize.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.faceMesh = new FaceMeshLib({
|
if (!this.faceMesh) {
|
||||||
locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
|
const FaceMeshLib = (window as any).FaceMesh;
|
||||||
});
|
if (!FaceMeshLib) {
|
||||||
|
throw new Error('MediaPipe FaceMesh library failed to initialize.');
|
||||||
|
}
|
||||||
|
|
||||||
this.faceMesh.setOptions({
|
this.faceMesh = new FaceMeshLib({
|
||||||
maxNumFaces: 1,
|
locateFile: (file: string) => `assets/mediapipe/${file}`
|
||||||
refineLandmarks: false,
|
});
|
||||||
minDetectionConfidence: 0.6,
|
|
||||||
minTrackingConfidence: 0.6
|
|
||||||
});
|
|
||||||
|
|
||||||
this.faceMesh.onResults((results: any) => {
|
this.faceMesh.setOptions({
|
||||||
this.processLandmarks(results);
|
maxNumFaces: 1,
|
||||||
});
|
refineLandmarks: false,
|
||||||
|
minDetectionConfidence: 0.6,
|
||||||
|
minTrackingConfidence: 0.6
|
||||||
|
});
|
||||||
|
|
||||||
this.camera = new CameraLib(videoElement, {
|
this.faceMesh.onResults((results: any) => {
|
||||||
onFrame: async () => {
|
this.processLandmarks(results);
|
||||||
if (this.isCameraActive() && this.faceMesh) {
|
});
|
||||||
await this.faceMesh.send({ image: videoElement });
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
width: 320,
|
|
||||||
height: 240
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isCameraActive.set(true);
|
this.isCameraActive.set(true);
|
||||||
await this.camera.start();
|
|
||||||
|
// Start custom frame processing loop
|
||||||
|
this.stopFrameLoop();
|
||||||
|
this.startFrameLoop(videoElement);
|
||||||
|
|
||||||
console.log('[FaceDetector] Face tracking started successfully.');
|
console.log('[FaceDetector] Face tracking started successfully.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[FaceDetector] Failed to start face tracking:', err);
|
console.error('[FaceDetector] Failed to start face tracking:', err);
|
||||||
@@ -122,6 +117,38 @@ export class FaceDetectorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private stopFrameLoop() {
|
||||||
|
if (this.animFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.animFrameId);
|
||||||
|
this.animFrameId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startFrameLoop(videoElement: HTMLVideoElement) {
|
||||||
|
let lastFrameTime = 0;
|
||||||
|
const FRAME_INTERVAL_MS = 100; // Analizza massimo 10 fotogrammi al secondo per risparmiare CPU/RAM
|
||||||
|
|
||||||
|
const processFrame = async () => {
|
||||||
|
if (!this.isCameraActive()) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastFrameTime >= FRAME_INTERVAL_MS && this.faceMesh && videoElement && videoElement.readyState >= 2) {
|
||||||
|
lastFrameTime = now;
|
||||||
|
try {
|
||||||
|
await this.faceMesh.send({ image: videoElement });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[FaceDetector] Error processing frame:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isCameraActive()) {
|
||||||
|
this.animFrameId = requestAnimationFrame(processFrame);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animFrameId = requestAnimationFrame(processFrame);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes landmarks to calculate head tilt angle.
|
* Processes landmarks to calculate head tilt angle.
|
||||||
*/
|
*/
|
||||||
@@ -155,7 +182,7 @@ export class FaceDetectorService {
|
|||||||
if (absAngle > this.TILT_THRESHOLD) {
|
if (absAngle > this.TILT_THRESHOLD) {
|
||||||
this.isTilted.set(true);
|
this.isTilted.set(true);
|
||||||
|
|
||||||
if (!this.inCooldown) {
|
if (!this.inCooldown && (Date.now() - this.lastTriggerTime > this.TRIGGER_COOLDOWN_MS)) {
|
||||||
if (this.tiltStartTime === 0) {
|
if (this.tiltStartTime === 0) {
|
||||||
this.tiltStartTime = Date.now();
|
this.tiltStartTime = Date.now();
|
||||||
} else if (Date.now() - this.tiltStartTime > this.TILT_HOLD_MS) {
|
} else if (Date.now() - this.tiltStartTime > this.TILT_HOLD_MS) {
|
||||||
@@ -167,6 +194,7 @@ export class FaceDetectorService {
|
|||||||
}
|
}
|
||||||
this.inCooldown = true;
|
this.inCooldown = true;
|
||||||
this.tiltStartTime = 0;
|
this.tiltStartTime = 0;
|
||||||
|
this.lastTriggerTime = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -181,21 +209,23 @@ export class FaceDetectorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops camera capture and releases face mesh resources.
|
* Pauses the frame tracking loop without killing the underlying media stream hardware.
|
||||||
|
*/
|
||||||
|
pause() {
|
||||||
|
this.stopFrameLoop();
|
||||||
|
this.isCameraActive.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops camera capture and releases face mesh & hardware stream resources.
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this.isCameraActive.set(false);
|
this.pause();
|
||||||
this.currentTiltAngle.set(0);
|
this.currentTiltAngle.set(0);
|
||||||
this.isTilted.set(false);
|
this.isTilted.set(false);
|
||||||
this.inCooldown = false;
|
this.inCooldown = false;
|
||||||
this.tiltStartTime = 0;
|
this.tiltStartTime = 0;
|
||||||
|
this.lastTriggerTime = 0;
|
||||||
if (this.camera) {
|
|
||||||
try {
|
|
||||||
this.camera.stop();
|
|
||||||
} catch (e) {}
|
|
||||||
this.camera = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.stream) {
|
if (this.stream) {
|
||||||
this.stream.getTracks().forEach(track => track.stop());
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
@@ -210,6 +240,6 @@ export class FaceDetectorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.onTiltCallback = null;
|
this.onTiltCallback = null;
|
||||||
console.log('[FaceDetector] Face tracking stopped.');
|
console.log('[FaceDetector] Face tracking fully stopped.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,4 +126,23 @@ Abba Padre!
|
|||||||
expect(sections[1].type).toBe('chorus');
|
expect(sections[1].type).toBe('chorus');
|
||||||
expect(sections[2].type).toBe('verse');
|
expect(sections[2].type).toBe('verse');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse vertical bars | as chords', () => {
|
||||||
|
const rawSong = `
|
||||||
|
[SOL] | [RE] | [DO] | [RE]
|
||||||
|
| | | |
|
||||||
|
`;
|
||||||
|
const sections = service.parseAccordi(rawSong);
|
||||||
|
expect(sections.length).toBe(1);
|
||||||
|
const line1 = sections[0].lines[0];
|
||||||
|
const line2 = sections[0].lines[1];
|
||||||
|
|
||||||
|
// Check first line: SOL | RE | DO | RE
|
||||||
|
// All components should be parsed as chords
|
||||||
|
expect(line1.segments.map(s => s.chord)).toEqual(['SOL', '|', 'RE', '|', 'DO', '|', 'RE']);
|
||||||
|
expect(line1.segments.map(s => s.text.trim())).toEqual(['', '', '', '', '', '', '']);
|
||||||
|
|
||||||
|
// Check second line: | | | |
|
||||||
|
expect(line2.segments.map(s => s.chord)).toEqual(['|', '|', '|', '|']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -194,9 +194,13 @@ export class LyricsParserService {
|
|||||||
* sei Re Gesù[SOL] → text "sei Re Gesù" then chord "SOL" with empty text
|
* sei Re Gesù[SOL] → text "sei Re Gesù" then chord "SOL" with empty text
|
||||||
*/
|
*/
|
||||||
parseChordLine(line: string): ParsedLine {
|
parseChordLine(line: string): ParsedLine {
|
||||||
|
// Treat vertical bars (|) as chords instead of text
|
||||||
|
let prepared = line.replace(/\[\|\]/g, '|');
|
||||||
|
prepared = prepared.replace(/\|/g, '[|]');
|
||||||
|
|
||||||
const segments: ChordSegment[] = [];
|
const segments: ChordSegment[] = [];
|
||||||
// Clean up non-breaking spaces
|
// Clean up non-breaking spaces
|
||||||
const cleaned = line.replace(/\u00a0/g, ' ').trim();
|
const cleaned = prepared.replace(/\u00a0/g, ' ').trim();
|
||||||
|
|
||||||
// Regex to match [CHORD] tags and text between them
|
// Regex to match [CHORD] tags and text between them
|
||||||
const chordRegex = /\[([^\]]+)\]/g;
|
const chordRegex = /\[([^\]]+)\]/g;
|
||||||
@@ -251,6 +255,7 @@ export class LyricsParserService {
|
|||||||
*/
|
*/
|
||||||
transposeChord(chord: string, semitones: number): string {
|
transposeChord(chord: string, semitones: number): string {
|
||||||
if (!chord) return chord;
|
if (!chord) return chord;
|
||||||
|
if (chord === '|') return '|';
|
||||||
|
|
||||||
// Convert Italian chords ending in 'M' or 'N' (e.g. LAM -> LAm, LAN -> LAm, LAN7 -> LAm7) to lowercase 'm'
|
// Convert Italian chords ending in 'M' or 'N' (e.g. LAM -> LAm, LAN -> LAm, LAN7 -> LAm7) to lowercase 'm'
|
||||||
chord = chord.replace(/\b(DO|RE|MI|FA|SOL|LA|SI)(#|b|♭)?[mMnN](\d*)\b/g, (match, p1, p2, p3) => {
|
chord = chord.replace(/\b(DO|RE|MI|FA|SOL|LA|SI)(#|b|♭)?[mMnN](\d*)\b/g, (match, p1, p2, p3) => {
|
||||||
@@ -340,6 +345,11 @@ export class LyricsParserService {
|
|||||||
isContiguousNext(segments: ChordSegment[], index: number): boolean {
|
isContiguousNext(segments: ChordSegment[], index: number): boolean {
|
||||||
if (!segments || index >= segments.length - 1) return false;
|
if (!segments || index >= segments.length - 1) return false;
|
||||||
|
|
||||||
|
// Se sia il segmento corrente che il successivo hanno un accordo, non sono contigui (vogliamo dello spazio tra loro)
|
||||||
|
if (segments[index].chord && segments[index + 1].chord) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the next segment with non-empty text
|
// Find the next segment with non-empty text
|
||||||
let nextWithText: ChordSegment | null = null;
|
let nextWithText: ChordSegment | null = null;
|
||||||
for (let i = index + 1; i < segments.length; i++) {
|
for (let i = index + 1; i < segments.length; i++) {
|
||||||
@@ -362,5 +372,42 @@ export class LyricsParserService {
|
|||||||
|
|
||||||
return endsWithNonSpace && startsWithNonSpace;
|
return endsWithNonSpace && startsWithNonSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduce the musical key (tonality) of the song by looking at the first chord.
|
||||||
|
*/
|
||||||
|
deduceTonality(raw: string): string | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
const matches = [...raw.matchAll(/\[([^\]]+)\]/g)];
|
||||||
|
if (matches.length === 0) return null;
|
||||||
|
|
||||||
|
// Get the first chord
|
||||||
|
let firstChord = matches[0][1].trim();
|
||||||
|
if (firstChord.includes('/')) {
|
||||||
|
firstChord = firstChord.split('/')[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize minor indicators (uppercase M/N to m)
|
||||||
|
firstChord = firstChord.replace(/\b(DO|RE|MI|FA|SOL|LA|SI)(#|b|♭)?[mMnN](\d*)\b/g, (match, p1, p2, p3) => {
|
||||||
|
return p1 + (p2 || '') + 'm';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the root chord name matching the scales
|
||||||
|
const possibleRoots = [...this.scale, ...this.flatScale].sort((a, b) => b.length - a.length);
|
||||||
|
let root = '';
|
||||||
|
const upperChord = firstChord.toUpperCase();
|
||||||
|
for (const r of possibleRoots) {
|
||||||
|
if (upperChord.startsWith(r.toUpperCase())) {
|
||||||
|
root = r;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root) return null;
|
||||||
|
|
||||||
|
// Check if it is a minor chord
|
||||||
|
const isMinor = firstChord.toLowerCase().includes('m') || firstChord.includes('-');
|
||||||
|
return root + (isMinor ? 'm' : '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,17 +17,21 @@ export class MyCantiService {
|
|||||||
|
|
||||||
private _storage: Storage | null = null;
|
private _storage: Storage | null = null;
|
||||||
public myCanti = signal<Canto[]>([]);
|
public myCanti = signal<Canto[]>([]);
|
||||||
|
private initPromise!: Promise<void>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.initPromise = this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init(): Promise<void> {
|
||||||
this._storage = this.cantiService.getStorage();
|
this._storage = this.cantiService.getStorage();
|
||||||
if (!this._storage) {
|
if (!this._storage) {
|
||||||
// If CantiService hasn't initialized storage yet, wait a bit
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(() => this.init(), 500);
|
setTimeout(async () => {
|
||||||
return;
|
await this.init();
|
||||||
|
resolve();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const saved = await this._storage.get('my-canti');
|
const saved = await this._storage.get('my-canti');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -36,6 +40,7 @@ export class MyCantiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveCanto(canto: Partial<Canto>): Promise<Canto> {
|
async saveCanto(canto: Partial<Canto>): Promise<Canto> {
|
||||||
|
await this.initPromise;
|
||||||
const current = this.myCanti();
|
const current = this.myCanti();
|
||||||
let updated: Canto[];
|
let updated: Canto[];
|
||||||
let targetCanto: Canto;
|
let targetCanto: Canto;
|
||||||
@@ -51,7 +56,9 @@ export class MyCantiService {
|
|||||||
accordi: canto.accordi !== undefined ? canto.accordi : c.accordi,
|
accordi: canto.accordi !== undefined ? canto.accordi : c.accordi,
|
||||||
autore: canto.autore !== undefined ? canto.autore : c.autore,
|
autore: canto.autore !== undefined ? canto.autore : c.autore,
|
||||||
link_youtube: canto.link_youtube !== undefined ? canto.link_youtube : c.link_youtube,
|
link_youtube: canto.link_youtube !== undefined ? canto.link_youtube : c.link_youtube,
|
||||||
id_momenti: canto.id_momenti || c.id_momenti
|
id_momenti: canto.id_momenti || c.id_momenti,
|
||||||
|
durata: canto.durata !== undefined ? canto.durata : c.durata,
|
||||||
|
bpm: canto.bpm !== undefined ? canto.bpm : c.bpm
|
||||||
};
|
};
|
||||||
return targetCanto;
|
return targetCanto;
|
||||||
}
|
}
|
||||||
@@ -69,7 +76,9 @@ export class MyCantiService {
|
|||||||
accordi: canto.accordi,
|
accordi: canto.accordi,
|
||||||
autore: canto.autore,
|
autore: canto.autore,
|
||||||
link_youtube: canto.link_youtube,
|
link_youtube: canto.link_youtube,
|
||||||
id_momenti: canto.id_momenti || []
|
id_momenti: canto.id_momenti || [],
|
||||||
|
durata: canto.durata,
|
||||||
|
bpm: canto.bpm
|
||||||
};
|
};
|
||||||
updated.push(targetCanto);
|
updated.push(targetCanto);
|
||||||
}
|
}
|
||||||
@@ -83,7 +92,9 @@ export class MyCantiService {
|
|||||||
accordi: canto.accordi,
|
accordi: canto.accordi,
|
||||||
autore: canto.autore,
|
autore: canto.autore,
|
||||||
link_youtube: canto.link_youtube,
|
link_youtube: canto.link_youtube,
|
||||||
id_momenti: canto.id_momenti || []
|
id_momenti: canto.id_momenti || [],
|
||||||
|
durata: canto.durata,
|
||||||
|
bpm: canto.bpm
|
||||||
};
|
};
|
||||||
updated = [...current, targetCanto];
|
updated = [...current, targetCanto];
|
||||||
}
|
}
|
||||||
@@ -108,6 +119,7 @@ export class MyCantiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteCanto(id: string) {
|
async deleteCanto(id: string) {
|
||||||
|
await this.initPromise;
|
||||||
const updated = this.myCanti().filter(c => c.id !== id);
|
const updated = this.myCanti().filter(c => c.id !== id);
|
||||||
this.myCanti.set(updated);
|
this.myCanti.set(updated);
|
||||||
await this._storage?.set('my-canti', updated);
|
await this._storage?.set('my-canti', updated);
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { Storage } from '@ionic/storage-angular';
|
|||||||
import { Canto, CantiService } from './canti.service';
|
import { Canto, CantiService } from './canti.service';
|
||||||
import { ComunitaService } from './comunita.service';
|
import { ComunitaService } from './comunita.service';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { ToastController, AlertController } from '@ionic/angular';
|
import { ToastController, AlertController, ModalController } from '@ionic/angular';
|
||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from './settings.service';
|
||||||
|
import { SharePlaylistQrModalComponent } from '../components/share-playlist-qr-modal/share-playlist-qr-modal.component';
|
||||||
import { MyCantiService } from './my-canti.service';
|
import { MyCantiService } from './my-canti.service';
|
||||||
|
import { LyricsParserService } from './lyrics-parser.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -13,9 +15,11 @@ import { MyCantiService } from './my-canti.service';
|
|||||||
export class PlaylistService {
|
export class PlaylistService {
|
||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
private cantiService = inject(CantiService);
|
private cantiService = inject(CantiService);
|
||||||
|
private lyricsParser = inject(LyricsParserService);
|
||||||
private comunitaService = inject(ComunitaService);
|
private comunitaService = inject(ComunitaService);
|
||||||
private toastCtrl = inject(ToastController);
|
private toastCtrl = inject(ToastController);
|
||||||
private alertCtrl = inject(AlertController);
|
private alertCtrl = inject(AlertController);
|
||||||
|
private modalCtrl = inject(ModalController);
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private injector = inject(Injector);
|
private injector = inject(Injector);
|
||||||
private myCantiService!: MyCantiService;
|
private myCantiService!: MyCantiService;
|
||||||
@@ -29,10 +33,12 @@ export class PlaylistService {
|
|||||||
public activeListIds = signal<string[]>([]);
|
public activeListIds = signal<string[]>([]);
|
||||||
public activeListName = signal<string | null>(null);
|
public activeListName = signal<string | null>(null);
|
||||||
public activePlaylistId = signal<string | null>(null);
|
public activePlaylistId = signal<string | null>(null);
|
||||||
|
public filteredListIds = signal<string[]>([]);
|
||||||
|
|
||||||
public remotePlaylist = signal<any | null>(null);
|
public remotePlaylist = signal<any | null>(null);
|
||||||
public remoteCustomSongs = signal<any[]>([]);
|
public remoteCustomSongs = signal<any[]>([]);
|
||||||
public remoteShareCanti = signal<Canto[]>([]);
|
public remoteShareCanti = signal<Canto[]>([]);
|
||||||
|
public hasRemotePlaylistUpdate = signal<boolean>(false);
|
||||||
|
|
||||||
private _storage: Storage | null = null;
|
private _storage: Storage | null = null;
|
||||||
private initPromise!: Promise<void>;
|
private initPromise!: Promise<void>;
|
||||||
@@ -43,10 +49,7 @@ export class PlaylistService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this.comunitaService.comunitaScalette().map(s => ({
|
return this.comunitaService.comunitaScalette().map(s => ({
|
||||||
id: s.id,
|
...s,
|
||||||
name: s.name,
|
|
||||||
ids: s.ids,
|
|
||||||
createdAt: s.date,
|
|
||||||
isComunita: true
|
isComunita: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -83,21 +86,23 @@ export class PlaylistService {
|
|||||||
|
|
||||||
getPlaylistsStorageKey(): string {
|
getPlaylistsStorageKey(): string {
|
||||||
const code = this.comunitaService.comunitaCode();
|
const code = this.comunitaService.comunitaCode();
|
||||||
const isCommunityActive = this.comunitaService.isFilterActive();
|
const active = this.comunitaService.isFilterActive();
|
||||||
if (code && isCommunityActive) {
|
if (code && active) {
|
||||||
return `playlists_comunita_${code}`;
|
return `playlists_${code}`;
|
||||||
}
|
}
|
||||||
return 'playlists';
|
return 'playlists';
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init(): Promise<void> {
|
||||||
const storage = await this.storage.create();
|
this._storage = this.cantiService.getStorage();
|
||||||
this._storage = storage;
|
if (!this._storage) {
|
||||||
await this.loadPlaylistsForCurrentContext();
|
return new Promise<void>((resolve) => {
|
||||||
}
|
setTimeout(async () => {
|
||||||
|
await this.init();
|
||||||
async loadPlaylistsForCurrentContext() {
|
resolve();
|
||||||
if (!this._storage) return;
|
}, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
const key = this.getPlaylistsStorageKey();
|
const key = this.getPlaylistsStorageKey();
|
||||||
const saved = await this._storage.get(key);
|
const saved = await this._storage.get(key);
|
||||||
this.playlists.set(saved || []);
|
this.playlists.set(saved || []);
|
||||||
@@ -126,6 +131,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() {
|
async refreshRemotePlaylist() {
|
||||||
const remotePl = this.remotePlaylist();
|
const remotePl = this.remotePlaylist();
|
||||||
if (!remotePl) return;
|
if (!remotePl) return;
|
||||||
@@ -149,7 +221,9 @@ export class PlaylistService {
|
|||||||
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
accordi: item.accordi || (item.testo?.includes('[') ? item.testo : undefined),
|
||||||
autore: item.autore || '',
|
autore: item.autore || '',
|
||||||
link_youtube: item.link_youtube || '',
|
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
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const playlists = remoteJson
|
const playlists = remoteJson
|
||||||
@@ -177,6 +251,12 @@ export class PlaylistService {
|
|||||||
this.remoteCustomSongs.set(customSongs);
|
this.remoteCustomSongs.set(customSongs);
|
||||||
await this._storage?.set('remote_playlist', updatedPl);
|
await this._storage?.set('remote_playlist', updatedPl);
|
||||||
await this._storage?.set('remote_custom_songs', customSongs);
|
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.');
|
console.log('[Remote-Sync] Remote playlist and custom songs updated successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,15 +408,14 @@ export class PlaylistService {
|
|||||||
this.syncLocalDataToServer();
|
this.syncLocalDataToServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePlaylistSongSettings(playlistId: string, songId: string, tonalita: number, speed: number, zoom?: number) {
|
async updatePlaylistSongSettings(playlistId: string, songId: string, tonalita: number, speed: number) {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.playlists.update(p => p.map(pl => {
|
this.playlists.update(p => p.map(pl => {
|
||||||
if (pl.id === playlistId) {
|
if (pl.id === playlistId) {
|
||||||
const songSettings = { ...(pl.songSettings || {}) };
|
const songSettings = { ...(pl.songSettings || {}) };
|
||||||
songSettings[songId] = {
|
songSettings[songId] = {
|
||||||
tonalita,
|
tonalita,
|
||||||
speed,
|
speed
|
||||||
zoom: zoom !== undefined ? zoom : songSettings[songId]?.zoom
|
|
||||||
};
|
};
|
||||||
return { ...pl, songSettings };
|
return { ...pl, songSettings };
|
||||||
}
|
}
|
||||||
@@ -414,7 +493,9 @@ export class PlaylistService {
|
|||||||
accordi: c.accordi || '',
|
accordi: c.accordi || '',
|
||||||
momenti: c.id_momenti?.map(id => String(id)) || [],
|
momenti: c.id_momenti?.map(id => String(id)) || [],
|
||||||
periodi: [] as string[],
|
periodi: [] as string[],
|
||||||
testo: c.testo || ''
|
testo: c.testo || '',
|
||||||
|
durata: c.durata || '',
|
||||||
|
bpm: c.bpm !== undefined ? c.bpm : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -428,7 +509,9 @@ export class PlaylistService {
|
|||||||
accordi: c.accordi || '',
|
accordi: c.accordi || '',
|
||||||
momenti: c.id_momenti?.map(id => String(id)) || [],
|
momenti: c.id_momenti?.map(id => String(id)) || [],
|
||||||
periodi: [] as string[],
|
periodi: [] as string[],
|
||||||
testo: c.testo || ''
|
testo: c.testo || '',
|
||||||
|
durata: c.durata || '',
|
||||||
|
bpm: c.bpm !== undefined ? c.bpm : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -442,7 +525,9 @@ export class PlaylistService {
|
|||||||
accordi: c.accordi || '',
|
accordi: c.accordi || '',
|
||||||
momenti: c.id_momenti?.map((id: any) => String(id)) || [],
|
momenti: c.id_momenti?.map((id: any) => String(id)) || [],
|
||||||
periodi: [] as string[],
|
periodi: [] as string[],
|
||||||
testo: c.testo || ''
|
testo: c.testo || '',
|
||||||
|
durata: c.durata || '',
|
||||||
|
bpm: c.bpm !== undefined ? c.bpm : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -456,7 +541,9 @@ export class PlaylistService {
|
|||||||
accordi: c.accordi || '',
|
accordi: c.accordi || '',
|
||||||
momenti: c.id_momenti?.map((id: any) => String(id)) || [],
|
momenti: c.id_momenti?.map((id: any) => String(id)) || [],
|
||||||
periodi: [] as string[],
|
periodi: [] as string[],
|
||||||
testo: c.testo || ''
|
testo: c.testo || '',
|
||||||
|
durata: c.durata || '',
|
||||||
|
bpm: c.bpm !== undefined ? c.bpm : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,108 +670,26 @@ export class PlaylistService {
|
|||||||
const activeId = this.activePlaylistId() || Date.now().toString();
|
const activeId = this.activePlaylistId() || Date.now().toString();
|
||||||
const isRemote = activeId.startsWith('remote_');
|
const isRemote = activeId.startsWith('remote_');
|
||||||
|
|
||||||
const buttons: any[] = [
|
let shareLink = `https://www.canticristiani.it/?playlist-uid=${uid}&playlist-id=${activeId}&openFirst=1`;
|
||||||
{
|
if (isRemote) {
|
||||||
text: isRemote ? 'Condividi (Sola Lettura)' : 'Sola Lettura (Consultazione)',
|
let remoteUid = uid;
|
||||||
handler: () => {
|
let remotePid = activeId;
|
||||||
let shareLink = `https://www.canticristiani.it/?playlist-uid=${uid}&playlist-id=${activeId}&openFirst=1`;
|
const parts = activeId.split('_');
|
||||||
if (isRemote) {
|
if (parts.length >= 3) {
|
||||||
let remoteUid = uid;
|
remoteUid = parts[1];
|
||||||
let remotePid = activeId;
|
remotePid = parts[2];
|
||||||
const parts = activeId.split('_');
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
remoteUid = parts[1];
|
|
||||||
remotePid = parts[2];
|
|
||||||
}
|
|
||||||
shareLink = `https://www.canticristiani.it/?playlist-uid=${remoteUid}&playlist-id=${remotePid}&openFirst=1`;
|
|
||||||
}
|
|
||||||
this.executeShare(shareLink, name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
shareLink = `https://www.canticristiani.it/?playlist-uid=${remoteUid}&playlist-id=${remotePid}&openFirst=1`;
|
||||||
|
|
||||||
if (!isRemote) {
|
|
||||||
buttons.push({
|
|
||||||
text: 'Modifica (Collaborazione / Backup)',
|
|
||||||
handler: () => {
|
|
||||||
const shareLink = `https://www.canticristiani.it/?restore-uid=${uid}`;
|
|
||||||
this.executeShare(shareLink, name + ' (Editor)');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push({
|
const modal = await this.modalCtrl.create({
|
||||||
text: 'Annulla',
|
component: SharePlaylistQrModalComponent,
|
||||||
role: 'cancel'
|
componentProps: {
|
||||||
});
|
playlistName: name,
|
||||||
|
shareLink: shareLink
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
header: 'Condividi Playlist',
|
|
||||||
message: isRemote ? 'Condividi questa playlist in sola lettura:' : 'Scegli la modalità di condivisione della playlist:',
|
|
||||||
buttons: buttons
|
|
||||||
});
|
|
||||||
await alert.present();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeShare(shareLink: string, name: string) {
|
|
||||||
// Generate QR using the link
|
|
||||||
const qrImage = await QRCode.toDataURL(shareLink, {
|
|
||||||
width: 400,
|
|
||||||
margin: 2,
|
|
||||||
color: {
|
|
||||||
dark: '#2d3436',
|
|
||||||
light: '#ffffff'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await modal.present();
|
||||||
const fileName = `${name.toLowerCase().replace(/\s+/g, '_')}_qr.png`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(qrImage);
|
|
||||||
const blob = await res.blob();
|
|
||||||
const file = new File([blob], fileName, { type: 'image/png' });
|
|
||||||
|
|
||||||
const isMac = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent) ||
|
|
||||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
||||||
|
|
||||||
if (!isMac && navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) {
|
|
||||||
await navigator.share({
|
|
||||||
files: [file],
|
|
||||||
title: 'Playlist CantiCristiani',
|
|
||||||
text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla subito: ${shareLink}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback to simple share text or copy/download
|
|
||||||
if (navigator.share) {
|
|
||||||
await navigator.share({
|
|
||||||
title: 'Playlist CantiCristiani',
|
|
||||||
text: `Ecco la playlist: ${name}\n\nClicca qui per aprirla: ${shareLink}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback: copia il link negli appunti e scarica l'immagine del QR
|
|
||||||
try {
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
await navigator.clipboard.writeText(shareLink);
|
|
||||||
const toast = await this.toastCtrl.create({
|
|
||||||
message: 'Link playlist copiato negli appunti! QR Code scaricato.',
|
|
||||||
duration: 3000,
|
|
||||||
color: 'success'
|
|
||||||
});
|
|
||||||
await toast.present();
|
|
||||||
}
|
|
||||||
} catch (clipErr) {
|
|
||||||
console.warn('Failed to copy link to clipboard:', clipErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = qrImage;
|
|
||||||
link.download = fileName;
|
|
||||||
link.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Share failed', err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPlaylists() {
|
async loadPlaylists() {
|
||||||
@@ -710,4 +715,560 @@ export class PlaylistService {
|
|||||||
this.remoteShareCanti.set(updated);
|
this.remoteShareCanti.set(updated);
|
||||||
await this._storage?.set('remote_share_canti', updated);
|
await this._storage?.set('remote_share_canti', updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public printPlaylist(playlistName: string, songs: any[], showChords: boolean, transpositions?: { [songId: string]: number }) {
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
if (!printWindow) {
|
||||||
|
alert('Impossibile aprire la finestra di stampa. Abilita i popup nel browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.myCantiService) {
|
||||||
|
this.myCantiService = this.injector.get(MyCantiService);
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleHtml = playlistName ? `
|
||||||
|
<div class="cover-page">
|
||||||
|
<div class="cover-border">
|
||||||
|
<h1>Playlist: ${playlistName}</h1>
|
||||||
|
<p class="meta">Data: ${new Date().toLocaleDateString('it-IT')} • N. Canti: ${songs.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
let contentHtml = '';
|
||||||
|
|
||||||
|
for (const song of songs) {
|
||||||
|
if (!song) continue;
|
||||||
|
const isPersonal = song.id && song.id.startsWith('my_');
|
||||||
|
let songNum = '';
|
||||||
|
if (isPersonal) {
|
||||||
|
const myIndex = this.myCantiService ? this.myCantiService.myCanti().findIndex((c: any) => c.id === song.id) : -1;
|
||||||
|
songNum = myIndex !== -1 ? `Pers. ${myIndex + 1}` : 'Pers.';
|
||||||
|
} else {
|
||||||
|
songNum = song.id_canti || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is a community number
|
||||||
|
const commCode = this.comunitaService.comunitaCode();
|
||||||
|
const commActive = this.comunitaService.isFilterActive();
|
||||||
|
let commNum = '';
|
||||||
|
if (commCode && commActive) {
|
||||||
|
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||||
|
const info = cantiInfo.find((x: any) => x.id_canti === song.id_canti || x.id_canti === Number(song.id));
|
||||||
|
if (info && info.num_canto) {
|
||||||
|
commNum = info.num_canto.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayNum = commNum ? `${commNum} (${songNum})` : songNum;
|
||||||
|
|
||||||
|
// Determine the transposition amount (semitones)
|
||||||
|
let semitones = 0;
|
||||||
|
if (transpositions && transpositions[song.id] !== undefined) {
|
||||||
|
semitones = transpositions[song.id];
|
||||||
|
} else if (transpositions && transpositions[song.id_canti] !== undefined) {
|
||||||
|
semitones = transpositions[song.id_canti];
|
||||||
|
} else {
|
||||||
|
// Fallback to active playlist settings or community settings
|
||||||
|
const activePlaylistId = this.activePlaylistId();
|
||||||
|
let playlistSongSetting: any = null;
|
||||||
|
if (activePlaylistId) {
|
||||||
|
if (activePlaylistId.startsWith('remote_')) {
|
||||||
|
const pl = this.remotePlaylist();
|
||||||
|
if (pl && pl.songSettings && pl.songSettings[song.id]) {
|
||||||
|
playlistSongSetting = pl.songSettings[song.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pl = this.playlists().find(p => p.id === activePlaylistId);
|
||||||
|
if (pl && pl.songSettings && pl.songSettings[song.id]) {
|
||||||
|
playlistSongSetting = pl.songSettings[song.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlistSongSetting && playlistSongSetting.tonalita !== undefined) {
|
||||||
|
semitones = playlistSongSetting.tonalita;
|
||||||
|
} else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) {
|
||||||
|
const settings = this.comunitaService.comunitaCantiSettings();
|
||||||
|
const songSetting = settings.find(s => s.id_canti === song.id_canti || s.id_canti === Number(song.id));
|
||||||
|
if (songSetting && songSetting.tonalita !== undefined) {
|
||||||
|
semitones = songSetting.tonalita;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawText = showChords ? (song.accordi || song.testo) : song.testo;
|
||||||
|
let parsedSections = showChords ? this.lyricsParser.parseAccordi(rawText) : this.lyricsParser.parseText(rawText);
|
||||||
|
if (showChords && semitones !== 0) {
|
||||||
|
parsedSections = this.lyricsParser.transposeSections(parsedSections, semitones);
|
||||||
|
}
|
||||||
|
|
||||||
|
let songBodyHtml = '';
|
||||||
|
for (const section of parsedSections) {
|
||||||
|
const sectionClass = section.type === 'chorus' ? 'section-chorus' : 'section-verse';
|
||||||
|
let sectionHeader = '';
|
||||||
|
if (section.type === 'verse_num' && section.verseNumber) {
|
||||||
|
sectionHeader = `<span class="verse-number">${section.verseNumber}.</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let linesHtml = '';
|
||||||
|
for (const line of section.lines) {
|
||||||
|
let lineContentHtml = '';
|
||||||
|
if (showChords) {
|
||||||
|
for (let i = 0; i < line.segments.length; i++) {
|
||||||
|
const seg = line.segments[i];
|
||||||
|
const isCont = this.lyricsParser.isContiguousNext(line.segments, i);
|
||||||
|
const chordHtml = seg.chord ? `<span class="chord">${seg.chord}</span>` : '';
|
||||||
|
lineContentHtml += `<span class="chord-segment${isCont ? ' contiguous-next' : ''}">${chordHtml}<span class="seg-text">${seg.text || ''}</span></span>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lineContentHtml = line.text;
|
||||||
|
}
|
||||||
|
linesHtml += `<div class="lyric-line">${lineContentHtml}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
songBodyHtml += `<div class="section-container ${sectionClass}">${sectionHeader}${linesHtml}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHtml += `
|
||||||
|
<div class="canto-container">
|
||||||
|
<div class="canto-header">
|
||||||
|
<div class="canto-title-row">
|
||||||
|
<span class="canto-number">${displayNum}</span>
|
||||||
|
<span class="canto-title">${song.titolo}</span>
|
||||||
|
</div>
|
||||||
|
<div class="canto-meta">
|
||||||
|
${song.autore ? `<span>Autore: ${song.autore}</span>` : ''}
|
||||||
|
${song.durata ? `<span>Durata: ${song.durata}</span>` : ''}
|
||||||
|
${song.bpm ? `<span>BPM: ${song.bpm}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="canto-body">
|
||||||
|
${songBodyHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>${playlistName || 'Playlist'}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4 landscape;
|
||||||
|
margin: 1cm;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
.cover-page {
|
||||||
|
page-break-after: always;
|
||||||
|
width: 27.7cm;
|
||||||
|
height: 18.8cm;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1.5cm;
|
||||||
|
}
|
||||||
|
.cover-border {
|
||||||
|
border: 2px solid #cc6600;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.cover-border h1 {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #cc6600;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
.cover-border .meta {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.canto-container {
|
||||||
|
page-break-after: always;
|
||||||
|
width: 27.7cm;
|
||||||
|
height: 18.8cm;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.canto-container:last-child {
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
.canto-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: 2px solid #cc6600;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.canto-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.canto-number {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #cc6600;
|
||||||
|
}
|
||||||
|
.canto-title {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
.canto-meta {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #777;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.canto-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
column-count: 2;
|
||||||
|
column-gap: 40px;
|
||||||
|
column-fill: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.45;
|
||||||
|
font-size: 27px; /* Aumentato di 0.5em rispetto al valore iniziale di 18px per leggibilità a distanza */
|
||||||
|
}
|
||||||
|
.section-container {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.section-chorus {
|
||||||
|
border-left: 3.5px solid #cc6600;
|
||||||
|
padding-left: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.verse-number {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #cc6600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
font-size: 18px; /* Scalato per font-size 27px */
|
||||||
|
}
|
||||||
|
.lyric-line {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
min-height: 1.2em;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
}
|
||||||
|
.chord-segment {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 0.22em;
|
||||||
|
}
|
||||||
|
.chord-segment.contiguous-next {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
.chord {
|
||||||
|
font-size: 0.82em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #b35900;
|
||||||
|
height: 1.3em;
|
||||||
|
margin-bottom: -0.1em;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.seg-text {
|
||||||
|
white-space: pre;
|
||||||
|
.close-btn-web {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #cc6600;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 9999;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn-web:hover {
|
||||||
|
background: #b35900;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
margin: 1cm;
|
||||||
|
size: A4 landscape;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.canto-container {
|
||||||
|
height: 18.8cm;
|
||||||
|
}
|
||||||
|
.close-btn-web {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button onclick="window.close()" class="close-btn-web">Chiudi</button>
|
||||||
|
${titleHtml}
|
||||||
|
${contentHtml}
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
const containers = Array.from(document.querySelectorAll('.canto-container'));
|
||||||
|
|
||||||
|
containers.forEach(container => {
|
||||||
|
const baseBody = container.querySelector('.canto-body');
|
||||||
|
if (!baseBody) return;
|
||||||
|
|
||||||
|
// Salva l'HTML originale per poterlo ripristinare intatto a ogni ciclo di ridimensionamento
|
||||||
|
let originalHtml = container.getAttribute('data-original-html');
|
||||||
|
if (!originalHtml) {
|
||||||
|
originalHtml = baseBody.innerHTML;
|
||||||
|
container.setAttribute('data-original-html', originalHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fontSize = 27; // Base 27px (+0.5em)
|
||||||
|
|
||||||
|
const overflows = (el) => {
|
||||||
|
return el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doLayoutSplit = () => {
|
||||||
|
// Rimuovi eventuali pagine dinamiche create in precedenza per questo canto
|
||||||
|
let next = container.nextSibling;
|
||||||
|
while (next && next.classList && next.classList.contains('dynamic-canto-page')) {
|
||||||
|
const toRemove = next;
|
||||||
|
next = next.nextSibling;
|
||||||
|
toRemove.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ripristina l'HTML originale intatto nella prima pagina
|
||||||
|
baseBody.innerHTML = originalHtml;
|
||||||
|
|
||||||
|
container.style.fontSize = fontSize + 'px';
|
||||||
|
baseBody.style.fontSize = fontSize + 'px';
|
||||||
|
|
||||||
|
const pages = [{ container, body: baseBody }];
|
||||||
|
let currentPageIndex = 0;
|
||||||
|
|
||||||
|
// Distribuisci il testo riga per riga (o sezione per sezione) creando nuove pagine
|
||||||
|
while (currentPageIndex < pages.length) {
|
||||||
|
const currentPage = pages[currentPageIndex];
|
||||||
|
|
||||||
|
if (overflows(currentPage.body)) {
|
||||||
|
let nextPageIndex = currentPageIndex + 1;
|
||||||
|
let nextPage = pages[nextPageIndex];
|
||||||
|
|
||||||
|
if (!nextPage) {
|
||||||
|
const newPageContainer = document.createElement('div');
|
||||||
|
newPageContainer.className = 'canto-container dynamic-canto-page';
|
||||||
|
|
||||||
|
const header = container.querySelector('.canto-header').cloneNode(true);
|
||||||
|
newPageContainer.appendChild(header);
|
||||||
|
|
||||||
|
const newBody = document.createElement('div');
|
||||||
|
newBody.className = 'canto-body';
|
||||||
|
newPageContainer.appendChild(newBody);
|
||||||
|
|
||||||
|
// Inserisci dopo l'ultima pagina inserita
|
||||||
|
const lastPageContainer = pages[pages.length - 1].container;
|
||||||
|
lastPageContainer.parentNode.insertBefore(newPageContainer, lastPageContainer.nextSibling);
|
||||||
|
|
||||||
|
nextPage = { container: newPageContainer, body: newBody };
|
||||||
|
pages.push(nextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imposta font-size coerente sulla nuova pagina
|
||||||
|
nextPage.container.style.fontSize = fontSize + 'px';
|
||||||
|
nextPage.body.style.fontSize = fontSize + 'px';
|
||||||
|
|
||||||
|
// Sposta elementi riga per riga per sfruttare al millimetro lo spazio
|
||||||
|
let safetyCounter = 0;
|
||||||
|
while (overflows(currentPage.body) && safetyCounter < 300) {
|
||||||
|
safetyCounter++;
|
||||||
|
const childSections = Array.from(currentPage.body.children);
|
||||||
|
if (childSections.length === 0) break;
|
||||||
|
|
||||||
|
const lastSection = childSections[childSections.length - 1];
|
||||||
|
const lyricLines = Array.from(lastSection.querySelectorAll('.lyric-line'));
|
||||||
|
|
||||||
|
// Recupera o assegna un ID univoco a questa sezione
|
||||||
|
let sectionId = lastSection.getAttribute('data-section-id');
|
||||||
|
if (!sectionId) {
|
||||||
|
sectionId = 'sec-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
lastSection.setAttribute('data-section-id', sectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cerca se esiste già la sezione corrispondente sulla pagina successiva
|
||||||
|
let targetSection = nextPage.body.querySelector('[data-section-id="' + sectionId + '"]');
|
||||||
|
if (!targetSection) {
|
||||||
|
targetSection = document.createElement('div');
|
||||||
|
targetSection.className = lastSection.className;
|
||||||
|
targetSection.setAttribute('data-section-id', sectionId);
|
||||||
|
nextPage.body.insertBefore(targetSection, nextPage.body.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lyricLines.length > 1) {
|
||||||
|
// Spezza la sezione: sposta solo l'ultima riga all'inizio di targetSection
|
||||||
|
const lastLine = lyricLines[lyricLines.length - 1];
|
||||||
|
targetSection.insertBefore(lastLine, targetSection.firstChild);
|
||||||
|
} else {
|
||||||
|
// Sposta tutti i restanti nodi (inclusi eventuali numeri di strofa o l'unica riga rimasta)
|
||||||
|
while (lastSection.firstChild) {
|
||||||
|
targetSection.insertBefore(lastSection.lastChild, targetSection.firstChild);
|
||||||
|
}
|
||||||
|
lastSection.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evita loop infiniti se una singola riga è più grande dell'intera pagina
|
||||||
|
const remaining = Array.from(currentPage.body.children);
|
||||||
|
if (remaining.length === 1) {
|
||||||
|
const remLines = remaining[0].querySelectorAll('.lyric-line');
|
||||||
|
if (remLines.length <= 1) {
|
||||||
|
// Sposta gli ultimi frammenti rimasti
|
||||||
|
let lastSec = remaining[0];
|
||||||
|
let secId = lastSec.getAttribute('data-section-id') || 'sec-last';
|
||||||
|
let finalTarget = nextPage.body.querySelector('[data-section-id="' + secId + '"]');
|
||||||
|
if (!finalTarget) {
|
||||||
|
finalTarget = document.createElement('div');
|
||||||
|
finalTarget.className = lastSec.className;
|
||||||
|
finalTarget.setAttribute('data-section-id', secId);
|
||||||
|
nextPage.body.insertBefore(finalTarget, nextPage.body.firstChild);
|
||||||
|
}
|
||||||
|
while (lastSec.firstChild) {
|
||||||
|
finalTarget.insertBefore(lastSec.lastChild, finalTarget.firstChild);
|
||||||
|
}
|
||||||
|
lastSec.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPageIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiorna le intestazioni con l'indice di pagina (es. 1 di 3, 2 di 3...)
|
||||||
|
const totalPages = pages.length;
|
||||||
|
pages.forEach((p, idx) => {
|
||||||
|
const titleElem = p.container.querySelector('.canto-title');
|
||||||
|
if (titleElem) {
|
||||||
|
let origTitle = titleElem.getAttribute('data-original-title');
|
||||||
|
if (!origTitle) {
|
||||||
|
origTitle = titleElem.innerHTML.split('<span')[0].trim();
|
||||||
|
titleElem.setAttribute('data-original-title', origTitle);
|
||||||
|
}
|
||||||
|
if (totalPages > 1) {
|
||||||
|
titleElem.innerHTML = origTitle + ' <span style="font-size: 0.65em; color: #777; font-weight: 400;">(' + (idx + 1) + ' di ' + totalPages + ')</span>';
|
||||||
|
} else {
|
||||||
|
titleElem.innerHTML = origTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSongPages = () => {
|
||||||
|
const pages = [container];
|
||||||
|
let next = container.nextSibling;
|
||||||
|
while (next && next.classList && next.classList.contains('dynamic-canto-page')) {
|
||||||
|
pages.push(next);
|
||||||
|
next = next.nextSibling;
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Esegui la suddivisione. Se qualcuna delle pagine risultanti trabocca, o
|
||||||
|
// se il canto supera il totale delle pagine massime consigliate, o
|
||||||
|
// se l'ultima pagina contiene solo 1 o 2 righe (orfane), riduci gradualmente il font size.
|
||||||
|
const checkAnyOverflow = () => {
|
||||||
|
const songPages = getSongPages();
|
||||||
|
|
||||||
|
// 1. Controlla se una pagina qualsiasi trabocca internamente
|
||||||
|
const hasInternalOverflow = songPages.some(p => {
|
||||||
|
const b = p.querySelector('.canto-body');
|
||||||
|
return b && overflows(b);
|
||||||
|
});
|
||||||
|
if (hasInternalOverflow) return true;
|
||||||
|
|
||||||
|
// 2. Limite pagine dinamico: preferisci massimo 3 pagine, consenti 4 o 5 solo se il carattere è già piccolo
|
||||||
|
let maxAllowedPages = 3;
|
||||||
|
if (fontSize <= 17) {
|
||||||
|
maxAllowedPages = 4;
|
||||||
|
}
|
||||||
|
if (fontSize <= 14) {
|
||||||
|
maxAllowedPages = 5;
|
||||||
|
}
|
||||||
|
if (songPages.length > maxAllowedPages) {
|
||||||
|
return true; // Supera il limite massimo consigliato per questo font size, forza la riduzione del font
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Se ci sono più pagine, controlla se l'ultima pagina ha solo 1 o 2 righe di testo
|
||||||
|
if (songPages.length > 1) {
|
||||||
|
const lastPage = songPages[songPages.length - 1];
|
||||||
|
const lastPageBody = lastPage.querySelector('.canto-body');
|
||||||
|
if (lastPageBody) {
|
||||||
|
const lyricLines = lastPageBody.querySelectorAll('.lyric-line').length;
|
||||||
|
if (lyricLines > 0 && lyricLines <= 2) {
|
||||||
|
return true; // Forza la riduzione del font per tirare su queste righe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
doLayoutSplit();
|
||||||
|
|
||||||
|
while (checkAnyOverflow() && fontSize > 12) {
|
||||||
|
fontSize -= 0.5;
|
||||||
|
doLayoutSplit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
}, 600);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
printWindow.document.open();
|
||||||
|
printWindow.document.write(html);
|
||||||
|
printWindow.document.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,18 @@ export class SettingsService {
|
|||||||
/** Visualizza data update sotto autore nella lista canti: true = attivo */
|
/** Visualizza data update sotto autore nella lista canti: true = attivo */
|
||||||
public showUpdateDate = signal<boolean>(true);
|
public showUpdateDate = signal<boolean>(true);
|
||||||
|
|
||||||
|
/** Visualizza durata, bpm e tonalità sotto autore / titolo: true = attivo */
|
||||||
|
public showDurationBpmTonality = signal<boolean>(true);
|
||||||
|
|
||||||
/** Attiva autoscroll standard nel dettaglio canto: true = attivo */
|
/** Attiva autoscroll standard nel dettaglio canto: true = attivo */
|
||||||
public enableStandardAutoscroll = signal<boolean>(false);
|
public enableStandardAutoscroll = signal<boolean>(false);
|
||||||
|
|
||||||
/** Attiva autoscroll visuale nel dettaglio canto: true = attivo */
|
/** Attiva autoscroll visuale nel dettaglio canto: true = attivo */
|
||||||
public enableVisualAutoscroll = signal<boolean>(true);
|
public enableVisualAutoscroll = signal<boolean>(true);
|
||||||
|
|
||||||
|
/** Navigazione con fotocamera (tracciamento testa) attiva nel player: true = attivo */
|
||||||
|
public cameraNavigationActive = signal<boolean>(false);
|
||||||
|
|
||||||
/** Preferenza notazione accordi: diesis o bemolle */
|
/** Preferenza notazione accordi: diesis o bemolle */
|
||||||
public chordNotationPreference = signal<'diesis' | 'bemolle'>('diesis');
|
public chordNotationPreference = signal<'diesis' | 'bemolle'>('diesis');
|
||||||
|
|
||||||
@@ -59,6 +65,9 @@ export class SettingsService {
|
|||||||
/** Schermo nero per l'ascolto delle playlist in macchina: true = attivo */
|
/** Schermo nero per l'ascolto delle playlist in macchina: true = attivo */
|
||||||
public carModeBlackScreen = signal<boolean>(false);
|
public carModeBlackScreen = signal<boolean>(false);
|
||||||
|
|
||||||
|
/** Global zoom/font size factor for song presentation */
|
||||||
|
public globalZoom = signal<number>(1.0);
|
||||||
|
|
||||||
/** Identificativo utente univoco per la gestione delle comunità */
|
/** Identificativo utente univoco per la gestione delle comunità */
|
||||||
public userUuid = signal<string>('');
|
public userUuid = signal<string>('');
|
||||||
|
|
||||||
@@ -131,6 +140,8 @@ export class SettingsService {
|
|||||||
this.deferredPrompt.set(e);
|
this.deferredPrompt.set(e);
|
||||||
// Update UI notify the user they can install the PWA
|
// Update UI notify the user they can install the PWA
|
||||||
this.showInstallButton.set(true);
|
this.showInstallButton.set(true);
|
||||||
|
// Se scatta l'evento di installazione, l'app NON è attualmente installata
|
||||||
|
localStorage.setItem('pwa-installed', 'false');
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('appinstalled', () => {
|
window.addEventListener('appinstalled', () => {
|
||||||
@@ -152,6 +163,7 @@ export class SettingsService {
|
|||||||
localStorage.setItem('invio-dati-statistici', 'false');
|
localStorage.setItem('invio-dati-statistici', 'false');
|
||||||
localStorage.setItem('show-tags-in-list', 'true');
|
localStorage.setItem('show-tags-in-list', 'true');
|
||||||
localStorage.setItem('show-update-date', 'true');
|
localStorage.setItem('show-update-date', 'true');
|
||||||
|
localStorage.setItem('show-duration-bpm-tonality', 'true');
|
||||||
localStorage.setItem('enable-standard-autoscroll', 'false');
|
localStorage.setItem('enable-standard-autoscroll', 'false');
|
||||||
localStorage.setItem('enable-visual-autoscroll', 'true');
|
localStorage.setItem('enable-visual-autoscroll', 'true');
|
||||||
localStorage.setItem('chord-notation-preference', 'diesis');
|
localStorage.setItem('chord-notation-preference', 'diesis');
|
||||||
@@ -222,6 +234,13 @@ export class SettingsService {
|
|||||||
this.showUpdateDate.set(true);
|
this.showUpdateDate.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedShowDurBpmTon = localStorage.getItem('show-duration-bpm-tonality');
|
||||||
|
if (savedShowDurBpmTon !== null) {
|
||||||
|
this.showDurationBpmTonality.set(savedShowDurBpmTon === 'true');
|
||||||
|
} else {
|
||||||
|
this.showDurationBpmTonality.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
const savedStandardAutoscroll = localStorage.getItem('enable-standard-autoscroll');
|
const savedStandardAutoscroll = localStorage.getItem('enable-standard-autoscroll');
|
||||||
if (savedStandardAutoscroll !== null) {
|
if (savedStandardAutoscroll !== null) {
|
||||||
this.enableStandardAutoscroll.set(savedStandardAutoscroll === 'true');
|
this.enableStandardAutoscroll.set(savedStandardAutoscroll === 'true');
|
||||||
@@ -264,6 +283,14 @@ export class SettingsService {
|
|||||||
this.carModeBlackScreen.set(false);
|
this.carModeBlackScreen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedGlobalZoom = localStorage.getItem('global-zoom');
|
||||||
|
if (savedGlobalZoom !== null) {
|
||||||
|
const parsed = parseFloat(savedGlobalZoom);
|
||||||
|
this.globalZoom.set(isNaN(parsed) ? 1.0 : parsed);
|
||||||
|
} else {
|
||||||
|
this.globalZoom.set(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
// 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 = !!(
|
||||||
@@ -301,6 +328,10 @@ export class SettingsService {
|
|||||||
localStorage.setItem('show-update-date', this.showUpdateDate().toString());
|
localStorage.setItem('show-update-date', this.showUpdateDate().toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
localStorage.setItem('show-duration-bpm-tonality', this.showDurationBpmTonality().toString());
|
||||||
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
localStorage.setItem('enable-standard-autoscroll', this.enableStandardAutoscroll().toString());
|
localStorage.setItem('enable-standard-autoscroll', this.enableStandardAutoscroll().toString());
|
||||||
});
|
});
|
||||||
@@ -325,6 +356,21 @@ export class SettingsService {
|
|||||||
localStorage.setItem('car-mode-black-screen', this.carModeBlackScreen().toString());
|
localStorage.setItem('car-mode-black-screen', this.carModeBlackScreen().toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
localStorage.setItem('global-zoom', this.globalZoom().toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedCameraNavActive = localStorage.getItem('camera-navigation-active');
|
||||||
|
if (savedCameraNavActive !== null) {
|
||||||
|
this.cameraNavigationActive.set(savedCameraNavActive === 'true');
|
||||||
|
} else {
|
||||||
|
this.cameraNavigationActive.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
localStorage.setItem('camera-navigation-active', this.cameraNavigationActive().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());
|
||||||
@@ -436,6 +482,12 @@ export class SettingsService {
|
|||||||
localStorage.setItem('show-update-date', newValue.toString());
|
localStorage.setItem('show-update-date', newValue.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleShowDurationBpmTonality() {
|
||||||
|
const newValue = !this.showDurationBpmTonality();
|
||||||
|
this.showDurationBpmTonality.set(newValue);
|
||||||
|
localStorage.setItem('show-duration-bpm-tonality', newValue.toString());
|
||||||
|
}
|
||||||
|
|
||||||
toggleStandardAutoscroll() {
|
toggleStandardAutoscroll() {
|
||||||
const newValue = !this.enableStandardAutoscroll();
|
const newValue = !this.enableStandardAutoscroll();
|
||||||
this.enableStandardAutoscroll.set(newValue);
|
this.enableStandardAutoscroll.set(newValue);
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ export class YoutubePlayerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public initPlayer(cantoId: string, startTime: number = 0, onEnded?: () => void, onError?: () => void) {
|
public initPlayer(cantoId: string, startTime: number = 0, onEnded?: () => void, onError?: () => void, customYoutubeLink?: string) {
|
||||||
if (this.currentCantoId() === cantoId && this.player) {
|
if (this.currentCantoId() === cantoId && this.player && !customYoutubeLink) {
|
||||||
this.onEndedCallback = onEnded || null;
|
this.onEndedCallback = onEnded || null;
|
||||||
this.onErrorCallback = onError || null;
|
this.onErrorCallback = onError || null;
|
||||||
|
|
||||||
@@ -123,36 +123,55 @@ export class YoutubePlayerService {
|
|||||||
this.onEndedCallback = onEnded || null;
|
this.onEndedCallback = onEnded || null;
|
||||||
this.onErrorCallback = onError || null;
|
this.onErrorCallback = onError || null;
|
||||||
|
|
||||||
let canto = this.cantiService.getCantoById(cantoId);
|
let videoId: string | null = null;
|
||||||
if (!canto) {
|
if (customYoutubeLink) {
|
||||||
canto = this.myCantiService.myCanti().find(c => c.id === cantoId);
|
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 (!videoId) {
|
||||||
if (onError) onError();
|
if (onError) onError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(window as any).YT || !(window as any).YT.Player) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse existing player if possible
|
// Reuse existing player if possible
|
||||||
if (this.player && this.player.loadVideoById) {
|
if (this.player && this.player.loadVideoById) {
|
||||||
this.currentCantoId.set(cantoId);
|
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({
|
this.player.loadVideoById({
|
||||||
videoId: videoId,
|
videoId: videoId,
|
||||||
startSeconds: startTime
|
startSeconds: startTime
|
||||||
@@ -163,7 +182,20 @@ export class YoutubePlayerService {
|
|||||||
|
|
||||||
this.destroyPlayer();
|
this.destroyPlayer();
|
||||||
this.currentCantoId.set(cantoId);
|
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', {
|
this.player = new (window as any).YT.Player('global-yt-player-container', {
|
||||||
height: '1',
|
height: '1',
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const VERSION = '2026.06.18.1434';
|
export const VERSION = '2026.08.09.0826';
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
(function(){/*
|
||||||
|
|
||||||
|
Copyright The Closure Library Authors.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
'use strict';function n(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}}var q="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,e){if(a==Array.prototype||a==Object.prototype)return a;a[b]=e.value;return a};
|
||||||
|
function t(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var e=a[b];if(e&&e.Math==Math)return e}throw Error("Cannot find global object");}var u=t(this);function v(a,b){if(b)a:{var e=u;a=a.split(".");for(var f=0;f<a.length-1;f++){var h=a[f];if(!(h in e))break a;e=e[h]}a=a[a.length-1];f=e[a];b=b(f);b!=f&&null!=b&&q(e,a,{configurable:!0,writable:!0,value:b})}}
|
||||||
|
v("Symbol",function(a){function b(l){if(this instanceof b)throw new TypeError("Symbol is not a constructor");return new e(f+(l||"")+"_"+h++,l)}function e(l,c){this.g=l;q(this,"description",{configurable:!0,writable:!0,value:c})}if(a)return a;e.prototype.toString=function(){return this.g};var f="jscomp_symbol_"+(1E9*Math.random()>>>0)+"_",h=0;return b});
|
||||||
|
v("Symbol.iterator",function(a){if(a)return a;a=Symbol("Symbol.iterator");for(var b="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),e=0;e<b.length;e++){var f=u[b[e]];"function"===typeof f&&"function"!=typeof f.prototype[a]&&q(f.prototype,a,{configurable:!0,writable:!0,value:function(){return w(n(this))}})}return a});function w(a){a={next:a};a[Symbol.iterator]=function(){return this};return a}
|
||||||
|
function x(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):{next:n(a)}}function y(){this.i=!1;this.g=null;this.o=void 0;this.j=1;this.m=0;this.h=null}function z(a){if(a.i)throw new TypeError("Generator is already running");a.i=!0}y.prototype.l=function(a){this.o=a};function A(a,b){a.h={F:b,G:!0};a.j=a.m}y.prototype.return=function(a){this.h={return:a};this.j=this.m};function B(a){this.g=new y;this.h=a}
|
||||||
|
function C(a,b){z(a.g);var e=a.g.g;if(e)return D(a,"return"in e?e["return"]:function(f){return{value:f,done:!0}},b,a.g.return);a.g.return(b);return H(a)}function D(a,b,e,f){try{var h=b.call(a.g.g,e);if(!(h instanceof Object))throw new TypeError("Iterator result "+h+" is not an object");if(!h.done)return a.g.i=!1,h;var l=h.value}catch(c){return a.g.g=null,A(a.g,c),H(a)}a.g.g=null;f.call(a.g,l);return H(a)}
|
||||||
|
function H(a){for(;a.g.j;)try{var b=a.h(a.g);if(b)return a.g.i=!1,{value:b.value,done:!1}}catch(e){a.g.o=void 0,A(a.g,e)}a.g.i=!1;if(a.g.h){b=a.g.h;a.g.h=null;if(b.G)throw b.F;return{value:b.return,done:!0}}return{value:void 0,done:!0}}
|
||||||
|
function I(a){this.next=function(b){z(a.g);a.g.g?b=D(a,a.g.g.next,b,a.g.l):(a.g.l(b),b=H(a));return b};this.throw=function(b){z(a.g);a.g.g?b=D(a,a.g.g["throw"],b,a.g.l):(A(a.g,b),b=H(a));return b};this.return=function(b){return C(a,b)};this[Symbol.iterator]=function(){return this}}function J(a){function b(f){return a.next(f)}function e(f){return a.throw(f)}return new Promise(function(f,h){function l(c){c.done?f(c.value):Promise.resolve(c.value).then(b,e).then(l,h)}l(a.next())})}
|
||||||
|
v("Promise",function(a){function b(c){this.h=0;this.i=void 0;this.g=[];this.o=!1;var d=this.j();try{c(d.resolve,d.reject)}catch(g){d.reject(g)}}function e(){this.g=null}function f(c){return c instanceof b?c:new b(function(d){d(c)})}if(a)return a;e.prototype.h=function(c){if(null==this.g){this.g=[];var d=this;this.i(function(){d.l()})}this.g.push(c)};var h=u.setTimeout;e.prototype.i=function(c){h(c,0)};e.prototype.l=function(){for(;this.g&&this.g.length;){var c=this.g;this.g=[];for(var d=0;d<c.length;++d){var g=
|
||||||
|
c[d];c[d]=null;try{g()}catch(k){this.j(k)}}}this.g=null};e.prototype.j=function(c){this.i(function(){throw c;})};b.prototype.j=function(){function c(k){return function(m){g||(g=!0,k.call(d,m))}}var d=this,g=!1;return{resolve:c(this.A),reject:c(this.l)}};b.prototype.A=function(c){if(c===this)this.l(new TypeError("A Promise cannot resolve to itself"));else if(c instanceof b)this.C(c);else{a:switch(typeof c){case "object":var d=null!=c;break a;case "function":d=!0;break a;default:d=!1}d?this.v(c):this.m(c)}};
|
||||||
|
b.prototype.v=function(c){var d=void 0;try{d=c.then}catch(g){this.l(g);return}"function"==typeof d?this.D(d,c):this.m(c)};b.prototype.l=function(c){this.u(2,c)};b.prototype.m=function(c){this.u(1,c)};b.prototype.u=function(c,d){if(0!=this.h)throw Error("Cannot settle("+c+", "+d+"): Promise already settled in state"+this.h);this.h=c;this.i=d;2===this.h&&this.B();this.H()};b.prototype.B=function(){var c=this;h(function(){if(c.I()){var d=u.console;"undefined"!==typeof d&&d.error(c.i)}},1)};b.prototype.I=
|
||||||
|
function(){if(this.o)return!1;var c=u.CustomEvent,d=u.Event,g=u.dispatchEvent;if("undefined"===typeof g)return!0;"function"===typeof c?c=new c("unhandledrejection",{cancelable:!0}):"function"===typeof d?c=new d("unhandledrejection",{cancelable:!0}):(c=u.document.createEvent("CustomEvent"),c.initCustomEvent("unhandledrejection",!1,!0,c));c.promise=this;c.reason=this.i;return g(c)};b.prototype.H=function(){if(null!=this.g){for(var c=0;c<this.g.length;++c)l.h(this.g[c]);this.g=null}};var l=new e;b.prototype.C=
|
||||||
|
function(c){var d=this.j();c.s(d.resolve,d.reject)};b.prototype.D=function(c,d){var g=this.j();try{c.call(d,g.resolve,g.reject)}catch(k){g.reject(k)}};b.prototype.then=function(c,d){function g(p,r){return"function"==typeof p?function(E){try{k(p(E))}catch(F){m(F)}}:r}var k,m,G=new b(function(p,r){k=p;m=r});this.s(g(c,k),g(d,m));return G};b.prototype.catch=function(c){return this.then(void 0,c)};b.prototype.s=function(c,d){function g(){switch(k.h){case 1:c(k.i);break;case 2:d(k.i);break;default:throw Error("Unexpected state: "+
|
||||||
|
k.h);}}var k=this;null==this.g?l.h(g):this.g.push(g);this.o=!0};b.resolve=f;b.reject=function(c){return new b(function(d,g){g(c)})};b.race=function(c){return new b(function(d,g){for(var k=x(c),m=k.next();!m.done;m=k.next())f(m.value).s(d,g)})};b.all=function(c){var d=x(c),g=d.next();return g.done?f([]):new b(function(k,m){function G(E){return function(F){p[E]=F;r--;0==r&&k(p)}}var p=[],r=0;do p.push(void 0),r++,f(g.value).s(G(p.length-1),m),g=d.next();while(!g.done)})};return b});
|
||||||
|
var K="function"==typeof Object.assign?Object.assign:function(a,b){for(var e=1;e<arguments.length;e++){var f=arguments[e];if(f)for(var h in f)Object.prototype.hasOwnProperty.call(f,h)&&(a[h]=f[h])}return a};v("Object.assign",function(a){return a||K});var L=this||self;var M={facingMode:"user",width:640,height:480};function N(a,b){this.video=a;this.i=0;this.h=Object.assign(Object.assign({},M),b)}N.prototype.stop=function(){var a=this,b,e,f,h;return J(new I(new B(function(l){if(a.g){b=a.g.getTracks();e=x(b);for(f=e.next();!f.done;f=e.next())h=f.value,h.stop();a.g=void 0}l.j=0})))};
|
||||||
|
N.prototype.start=function(){var a=this,b;return J(new I(new B(function(e){navigator.mediaDevices&&navigator.mediaDevices.getUserMedia||alert("No navigator.mediaDevices.getUserMedia exists.");b=a.h;return e.return(navigator.mediaDevices.getUserMedia({video:{facingMode:b.facingMode,width:b.width,height:b.height}}).then(function(f){O(a,f)}).catch(function(f){var h="Failed to acquire camera feed: "+f;console.error(h);alert(h);throw f;}))})))};
|
||||||
|
function P(a){window.requestAnimationFrame(function(){Q(a)})}function O(a,b){a.g=b;a.video.srcObject=b;a.video.onloadedmetadata=function(){a.video.play();P(a)}}function Q(a){var b=null;a.video.paused||a.video.currentTime===a.i||(a.i=a.video.currentTime,b=a.h.onFrame());b?b.then(function(){P(a)}):P(a)}var R=["Camera"],S=L;R[0]in S||"undefined"==typeof S.execScript||S.execScript("var "+R[0]);
|
||||||
|
for(var T;R.length&&(T=R.shift());)R.length||void 0===N?S[T]&&S[T]!==Object.prototype[T]?S=S[T]:S=S[T]={}:S[T]=N;}).call(this);
|
||||||
Binary file not shown.
@@ -0,0 +1,131 @@
|
|||||||
|
(function(){/*
|
||||||
|
|
||||||
|
Copyright The Closure Library Authors.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
'use strict';var v;function aa(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}}var ba="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};
|
||||||
|
function ca(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");}var G=ca(this);function J(a,b){if(b)a:{var c=G;a=a.split(".");for(var d=0;d<a.length-1;d++){var e=a[d];if(!(e in c))break a;c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&ba(c,a,{configurable:!0,writable:!0,value:b})}}
|
||||||
|
J("Symbol",function(a){function b(g){if(this instanceof b)throw new TypeError("Symbol is not a constructor");return new c(d+(g||"")+"_"+e++,g)}function c(g,f){this.g=g;ba(this,"description",{configurable:!0,writable:!0,value:f})}if(a)return a;c.prototype.toString=function(){return this.g};var d="jscomp_symbol_"+(1E9*Math.random()>>>0)+"_",e=0;return b});
|
||||||
|
J("Symbol.iterator",function(a){if(a)return a;a=Symbol("Symbol.iterator");for(var b="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),c=0;c<b.length;c++){var d=G[b[c]];"function"===typeof d&&"function"!=typeof d.prototype[a]&&ba(d.prototype,a,{configurable:!0,writable:!0,value:function(){return da(aa(this))}})}return a});function da(a){a={next:a};a[Symbol.iterator]=function(){return this};return a}
|
||||||
|
function K(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):{next:aa(a)}}function L(a){if(!(a instanceof Array)){a=K(a);for(var b,c=[];!(b=a.next()).done;)c.push(b.value);a=c}return a}var ea="function"==typeof Object.create?Object.create:function(a){function b(){}b.prototype=a;return new b},fa;
|
||||||
|
if("function"==typeof Object.setPrototypeOf)fa=Object.setPrototypeOf;else{var ha;a:{var ia={a:!0},ja={};try{ja.__proto__=ia;ha=ja.a;break a}catch(a){}ha=!1}fa=ha?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null}var ka=fa;
|
||||||
|
function M(a,b){a.prototype=ea(b.prototype);a.prototype.constructor=a;if(ka)ka(a,b);else for(var c in b)if("prototype"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.ea=b.prototype}function ma(){this.l=!1;this.i=null;this.h=void 0;this.g=1;this.s=this.m=0;this.j=null}function na(a){if(a.l)throw new TypeError("Generator is already running");a.l=!0}ma.prototype.o=function(a){this.h=a};
|
||||||
|
function oa(a,b){a.j={U:b,V:!0};a.g=a.m||a.s}ma.prototype.return=function(a){this.j={return:a};this.g=this.s};function N(a,b,c){a.g=c;return{value:b}}function pa(a){this.g=new ma;this.h=a}function qa(a,b){na(a.g);var c=a.g.i;if(c)return ra(a,"return"in c?c["return"]:function(d){return{value:d,done:!0}},b,a.g.return);a.g.return(b);return sa(a)}
|
||||||
|
function ra(a,b,c,d){try{var e=b.call(a.g.i,c);if(!(e instanceof Object))throw new TypeError("Iterator result "+e+" is not an object");if(!e.done)return a.g.l=!1,e;var g=e.value}catch(f){return a.g.i=null,oa(a.g,f),sa(a)}a.g.i=null;d.call(a.g,g);return sa(a)}function sa(a){for(;a.g.g;)try{var b=a.h(a.g);if(b)return a.g.l=!1,{value:b.value,done:!1}}catch(c){a.g.h=void 0,oa(a.g,c)}a.g.l=!1;if(a.g.j){b=a.g.j;a.g.j=null;if(b.V)throw b.U;return{value:b.return,done:!0}}return{value:void 0,done:!0}}
|
||||||
|
function ta(a){this.next=function(b){na(a.g);a.g.i?b=ra(a,a.g.i.next,b,a.g.o):(a.g.o(b),b=sa(a));return b};this.throw=function(b){na(a.g);a.g.i?b=ra(a,a.g.i["throw"],b,a.g.o):(oa(a.g,b),b=sa(a));return b};this.return=function(b){return qa(a,b)};this[Symbol.iterator]=function(){return this}}function O(a,b){b=new ta(new pa(b));ka&&a.prototype&&ka(b,a.prototype);return b}
|
||||||
|
function ua(a,b){a instanceof String&&(a+="");var c=0,d=!1,e={next:function(){if(!d&&c<a.length){var g=c++;return{value:b(g,a[g]),done:!1}}d=!0;return{done:!0,value:void 0}}};e[Symbol.iterator]=function(){return e};return e}var va="function"==typeof Object.assign?Object.assign:function(a,b){for(var c=1;c<arguments.length;c++){var d=arguments[c];if(d)for(var e in d)Object.prototype.hasOwnProperty.call(d,e)&&(a[e]=d[e])}return a};J("Object.assign",function(a){return a||va});
|
||||||
|
J("Promise",function(a){function b(f){this.h=0;this.i=void 0;this.g=[];this.o=!1;var h=this.j();try{f(h.resolve,h.reject)}catch(k){h.reject(k)}}function c(){this.g=null}function d(f){return f instanceof b?f:new b(function(h){h(f)})}if(a)return a;c.prototype.h=function(f){if(null==this.g){this.g=[];var h=this;this.i(function(){h.l()})}this.g.push(f)};var e=G.setTimeout;c.prototype.i=function(f){e(f,0)};c.prototype.l=function(){for(;this.g&&this.g.length;){var f=this.g;this.g=[];for(var h=0;h<f.length;++h){var k=
|
||||||
|
f[h];f[h]=null;try{k()}catch(l){this.j(l)}}}this.g=null};c.prototype.j=function(f){this.i(function(){throw f;})};b.prototype.j=function(){function f(l){return function(n){k||(k=!0,l.call(h,n))}}var h=this,k=!1;return{resolve:f(this.C),reject:f(this.l)}};b.prototype.C=function(f){if(f===this)this.l(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof b)this.F(f);else{a:switch(typeof f){case "object":var h=null!=f;break a;case "function":h=!0;break a;default:h=!1}h?this.u(f):this.m(f)}};
|
||||||
|
b.prototype.u=function(f){var h=void 0;try{h=f.then}catch(k){this.l(k);return}"function"==typeof h?this.G(h,f):this.m(f)};b.prototype.l=function(f){this.s(2,f)};b.prototype.m=function(f){this.s(1,f)};b.prototype.s=function(f,h){if(0!=this.h)throw Error("Cannot settle("+f+", "+h+"): Promise already settled in state"+this.h);this.h=f;this.i=h;2===this.h&&this.D();this.A()};b.prototype.D=function(){var f=this;e(function(){if(f.B()){var h=G.console;"undefined"!==typeof h&&h.error(f.i)}},1)};b.prototype.B=
|
||||||
|
function(){if(this.o)return!1;var f=G.CustomEvent,h=G.Event,k=G.dispatchEvent;if("undefined"===typeof k)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):"function"===typeof h?f=new h("unhandledrejection",{cancelable:!0}):(f=G.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.i;return k(f)};b.prototype.A=function(){if(null!=this.g){for(var f=0;f<this.g.length;++f)g.h(this.g[f]);this.g=null}};var g=new c;b.prototype.F=
|
||||||
|
function(f){var h=this.j();f.J(h.resolve,h.reject)};b.prototype.G=function(f,h){var k=this.j();try{f.call(h,k.resolve,k.reject)}catch(l){k.reject(l)}};b.prototype.then=function(f,h){function k(w,r){return"function"==typeof w?function(y){try{l(w(y))}catch(m){n(m)}}:r}var l,n,u=new b(function(w,r){l=w;n=r});this.J(k(f,l),k(h,n));return u};b.prototype.catch=function(f){return this.then(void 0,f)};b.prototype.J=function(f,h){function k(){switch(l.h){case 1:f(l.i);break;case 2:h(l.i);break;default:throw Error("Unexpected state: "+
|
||||||
|
l.h);}}var l=this;null==this.g?g.h(k):this.g.push(k);this.o=!0};b.resolve=d;b.reject=function(f){return new b(function(h,k){k(f)})};b.race=function(f){return new b(function(h,k){for(var l=K(f),n=l.next();!n.done;n=l.next())d(n.value).J(h,k)})};b.all=function(f){var h=K(f),k=h.next();return k.done?d([]):new b(function(l,n){function u(y){return function(m){w[y]=m;r--;0==r&&l(w)}}var w=[],r=0;do w.push(void 0),r++,d(k.value).J(u(w.length-1),n),k=h.next();while(!k.done)})};return b});
|
||||||
|
J("Object.is",function(a){return a?a:function(b,c){return b===c?0!==b||1/b===1/c:b!==b&&c!==c}});J("Array.prototype.includes",function(a){return a?a:function(b,c){var d=this;d instanceof String&&(d=String(d));var e=d.length;c=c||0;for(0>c&&(c=Math.max(c+e,0));c<e;c++){var g=d[c];if(g===b||Object.is(g,b))return!0}return!1}});
|
||||||
|
J("String.prototype.includes",function(a){return a?a:function(b,c){if(null==this)throw new TypeError("The 'this' value for String.prototype.includes must not be null or undefined");if(b instanceof RegExp)throw new TypeError("First argument to String.prototype.includes must not be a regular expression");return-1!==this.indexOf(b,c||0)}});J("Array.prototype.keys",function(a){return a?a:function(){return ua(this,function(b){return b})}});var wa=this||self;
|
||||||
|
function P(a,b){a=a.split(".");var c=wa;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}:c[d]=b};function xa(a,b){b=String.fromCharCode.apply(null,b);return null==a?b:a+b}var ya,za="undefined"!==typeof TextDecoder,Aa,Ba="undefined"!==typeof TextEncoder;
|
||||||
|
function Ca(a){if(Ba)a=(Aa||(Aa=new TextEncoder)).encode(a);else{var b=void 0;b=void 0===b?!1:b;for(var c=0,d=new Uint8Array(3*a.length),e=0;e<a.length;e++){var g=a.charCodeAt(e);if(128>g)d[c++]=g;else{if(2048>g)d[c++]=g>>6|192;else{if(55296<=g&&57343>=g){if(56319>=g&&e<a.length){var f=a.charCodeAt(++e);if(56320<=f&&57343>=f){g=1024*(g-55296)+f-56320+65536;d[c++]=g>>18|240;d[c++]=g>>12&63|128;d[c++]=g>>6&63|128;d[c++]=g&63|128;continue}else e--}if(b)throw Error("Found an unpaired surrogate");g=65533}d[c++]=
|
||||||
|
g>>12|224;d[c++]=g>>6&63|128}d[c++]=g&63|128}}a=d.subarray(0,c)}return a};var Da={},Ea=null;function Fa(a,b){void 0===b&&(b=0);Ga();b=Da[b];for(var c=Array(Math.floor(a.length/3)),d=b[64]||"",e=0,g=0;e<a.length-2;e+=3){var f=a[e],h=a[e+1],k=a[e+2],l=b[f>>2];f=b[(f&3)<<4|h>>4];h=b[(h&15)<<2|k>>6];k=b[k&63];c[g++]=l+f+h+k}l=0;k=d;switch(a.length-e){case 2:l=a[e+1],k=b[(l&15)<<2]||d;case 1:a=a[e],c[g]=b[a>>2]+b[(a&3)<<4|l>>4]+k+d}return c.join("")}
|
||||||
|
function Ha(a){var b=a.length,c=3*b/4;c%3?c=Math.floor(c):-1!="=.".indexOf(a[b-1])&&(c=-1!="=.".indexOf(a[b-2])?c-2:c-1);var d=new Uint8Array(c),e=0;Ia(a,function(g){d[e++]=g});return d.subarray(0,e)}
|
||||||
|
function Ia(a,b){function c(k){for(;d<a.length;){var l=a.charAt(d++),n=Ea[l];if(null!=n)return n;if(!/^[\s\xa0]*$/.test(l))throw Error("Unknown base64 encoding at char: "+l);}return k}Ga();for(var d=0;;){var e=c(-1),g=c(0),f=c(64),h=c(64);if(64===h&&-1===e)break;b(e<<2|g>>4);64!=f&&(b(g<<4&240|f>>2),64!=h&&b(f<<6&192|h))}}
|
||||||
|
function Ga(){if(!Ea){Ea={};for(var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(""),b=["+/=","+/","-_=","-_.","-_"],c=0;5>c;c++){var d=a.concat(b[c].split(""));Da[c]=d;for(var e=0;e<d.length;e++){var g=d[e];void 0===Ea[g]&&(Ea[g]=e)}}}};var Ja="function"===typeof Uint8Array.prototype.slice,Ka;function La(a,b,c){return b===c?Ka||(Ka=new Uint8Array(0)):Ja?a.slice(b,c):new Uint8Array(a.subarray(b,c))}var Q=0,R=0;function Ma(a,b){b=void 0===b?{}:b;b=void 0===b.v?!1:b.v;this.h=null;this.g=this.j=this.l=0;this.m=!1;this.v=b;a&&Na(this,a)}function Na(a,b){b=b.constructor===Uint8Array?b:b.constructor===ArrayBuffer?new Uint8Array(b):b.constructor===Array?new Uint8Array(b):b.constructor===String?Ha(b):b instanceof Uint8Array?new Uint8Array(b.buffer,b.byteOffset,b.byteLength):new Uint8Array(0);a.h=b;a.l=0;a.j=a.h.length;a.g=a.l}Ma.prototype.reset=function(){this.g=this.l};
|
||||||
|
function Oa(a){for(var b=128,c=0,d=0,e=0;4>e&&128<=b;e++)b=a.h[a.g++],c|=(b&127)<<7*e;128<=b&&(b=a.h[a.g++],c|=(b&127)<<28,d|=(b&127)>>4);if(128<=b)for(e=0;5>e&&128<=b;e++)b=a.h[a.g++],d|=(b&127)<<7*e+3;if(128>b){a=c>>>0;b=d>>>0;if(d=b&2147483648)a=~a+1>>>0,b=~b>>>0,0==a&&(b=b+1>>>0);a=4294967296*b+(a>>>0);return d?-a:a}a.m=!0}
|
||||||
|
Ma.prototype.i=function(){var a=this.h,b=a[this.g],c=b&127;if(128>b)return this.g+=1,c;b=a[this.g+1];c|=(b&127)<<7;if(128>b)return this.g+=2,c;b=a[this.g+2];c|=(b&127)<<14;if(128>b)return this.g+=3,c;b=a[this.g+3];c|=(b&127)<<21;if(128>b)return this.g+=4,c;b=a[this.g+4];c|=(b&15)<<28;if(128>b)return this.g+=5,c>>>0;this.g+=5;128<=a[this.g++]&&128<=a[this.g++]&&128<=a[this.g++]&&128<=a[this.g++]&&this.g++;return c};
|
||||||
|
Ma.prototype.o=function(){var a=this.h[this.g],b=this.h[this.g+1];var c=this.h[this.g+2];var d=this.h[this.g+3];this.g+=4;c=(a<<0|b<<8|c<<16|d<<24)>>>0;a=2*(c>>31)+1;b=c>>>23&255;c&=8388607;return 255==b?c?NaN:Infinity*a:0==b?a*Math.pow(2,-149)*c:a*Math.pow(2,b-150)*(c+Math.pow(2,23))};var Pa=[];function Qa(){this.g=new Uint8Array(64);this.h=0}Qa.prototype.push=function(a){if(!(this.h+1<this.g.length)){var b=this.g;this.g=new Uint8Array(Math.ceil(1+2*this.g.length));this.g.set(b)}this.g[this.h++]=a};Qa.prototype.length=function(){return this.h};Qa.prototype.end=function(){var a=this.g,b=this.h;this.h=0;return La(a,0,b)};function Ra(a,b){for(;127<b;)a.push(b&127|128),b>>>=7;a.push(b)};function Sa(a){var b={},c=void 0===b.N?!1:b.N;this.o={v:void 0===b.v?!1:b.v};this.N=c;b=this.o;Pa.length?(c=Pa.pop(),b&&(c.v=b.v),a&&Na(c,a),a=c):a=new Ma(a,b);this.g=a;this.m=this.g.g;this.h=this.i=this.l=-1;this.j=!1}Sa.prototype.reset=function(){this.g.reset();this.h=this.l=-1};function S(a){var b=a.g;(b=b.g==b.j)||(b=a.j)||(b=a.g,b=b.m||0>b.g||b.g>b.j);if(b)return!1;a.m=a.g.g;b=a.g.i();var c=b&7;if(0!=c&&5!=c&&1!=c&&2!=c&&3!=c&&4!=c)return a.j=!0,!1;a.i=b;a.l=b>>>3;a.h=c;return!0}
|
||||||
|
function Ta(a){switch(a.h){case 0:if(0!=a.h)Ta(a);else{for(a=a.g;a.h[a.g]&128;)a.g++;a.g++}break;case 1:1!=a.h?Ta(a):(a=a.g,a.g+=8);break;case 2:if(2!=a.h)Ta(a);else{var b=a.g.i();a=a.g;a.g+=b}break;case 5:5!=a.h?Ta(a):(a=a.g,a.g+=4);break;case 3:b=a.l;do{if(!S(a)){a.j=!0;break}if(4==a.h){a.l!=b&&(a.j=!0);break}Ta(a)}while(1);break;default:a.j=!0}}
|
||||||
|
function Ua(a,b,c){var d=a.g.j,e=a.g.i(),g=a.g.g+e;a.g.j=g;c(b,a);c=g-a.g.g;if(0!==c)throw Error("Message parsing ended unexpectedly. Expected to read "+e+" bytes, instead read "+(e-c)+" bytes, either the data ended unexpectedly or the message misreported its own length");a.g.g=g;a.g.j=d;return b}function T(a){return a.g.o()}
|
||||||
|
function Va(a){var b=a.g.i();a=a.g;var c=a.g;a.g+=b;a=a.h;var d;if(za)(d=ya)||(d=ya=new TextDecoder("utf-8",{fatal:!1})),d=d.decode(a.subarray(c,c+b));else{b=c+b;for(var e=[],g=null,f,h,k;c<b;)f=a[c++],128>f?e.push(f):224>f?c>=b?e.push(65533):(h=a[c++],194>f||128!==(h&192)?(c--,e.push(65533)):e.push((f&31)<<6|h&63)):240>f?c>=b-1?e.push(65533):(h=a[c++],128!==(h&192)||224===f&&160>h||237===f&&160<=h||128!==((d=a[c++])&192)?(c--,e.push(65533)):e.push((f&15)<<12|(h&63)<<6|d&63)):244>=f?c>=b-2?e.push(65533):
|
||||||
|
(h=a[c++],128!==(h&192)||0!==(f<<28)+(h-144)>>30||128!==((d=a[c++])&192)||128!==((k=a[c++])&192)?(c--,e.push(65533)):(f=(f&7)<<18|(h&63)<<12|(d&63)<<6|k&63,f-=65536,e.push((f>>10&1023)+55296,(f&1023)+56320))):e.push(65533),8192<=e.length&&(g=xa(g,e),e.length=0);d=xa(g,e)}return d}function Wa(a,b,c){var d=a.g.i();for(d=a.g.g+d;a.g.g<d;)c.push(b.call(a.g))}function Xa(a,b){2==a.h?Wa(a,Ma.prototype.o,b):b.push(T(a))};function Ya(){this.h=[];this.i=0;this.g=new Qa}function Za(a,b){0!==b.length&&(a.h.push(b),a.i+=b.length)}function $a(a){var b=a.i+a.g.length();if(0===b)return new Uint8Array(0);b=new Uint8Array(b);for(var c=a.h,d=c.length,e=0,g=0;g<d;g++){var f=c[g];0!==f.length&&(b.set(f,e),e+=f.length)}c=a.g;d=c.h;0!==d&&(b.set(c.g.subarray(0,d),e),c.h=0);a.h=[b];return b}
|
||||||
|
function U(a,b,c){if(null!=c){Ra(a.g,8*b+5);a=a.g;var d=c;d=(c=0>d?1:0)?-d:d;0===d?0<1/d?Q=R=0:(R=0,Q=2147483648):isNaN(d)?(R=0,Q=2147483647):3.4028234663852886E38<d?(R=0,Q=(c<<31|2139095040)>>>0):1.1754943508222875E-38>d?(d=Math.round(d/Math.pow(2,-149)),R=0,Q=(c<<31|d)>>>0):(b=Math.floor(Math.log(d)/Math.LN2),d*=Math.pow(2,-b),d=Math.round(8388608*d),16777216<=d&&++b,R=0,Q=(c<<31|b+127<<23|d&8388607)>>>0);c=Q;a.push(c>>>0&255);a.push(c>>>8&255);a.push(c>>>16&255);a.push(c>>>24&255)}};var ab="function"===typeof Uint8Array;function bb(a,b,c){if(null!=a)return"object"===typeof a?ab&&a instanceof Uint8Array?c(a):cb(a,b,c):b(a)}function cb(a,b,c){if(Array.isArray(a)){for(var d=Array(a.length),e=0;e<a.length;e++)d[e]=bb(a[e],b,c);Array.isArray(a)&&a.W&&db(d);return d}d={};for(e in a)d[e]=bb(a[e],b,c);return d}function eb(a){return"number"===typeof a?isFinite(a)?a:String(a):a}var fb={W:{value:!0,configurable:!0}};
|
||||||
|
function db(a){Array.isArray(a)&&!Object.isFrozen(a)&&Object.defineProperties(a,fb);return a};var gb;function V(a,b,c){var d=gb;gb=null;a||(a=d);d=this.constructor.ca;a||(a=d?[d]:[]);this.j=d?0:-1;this.m=this.g=null;this.h=a;a:{d=this.h.length;a=d-1;if(d&&(d=this.h[a],!(null===d||"object"!=typeof d||Array.isArray(d)||ab&&d instanceof Uint8Array))){this.l=a-this.j;this.i=d;break a}void 0!==b&&-1<b?(this.l=Math.max(b,a+1-this.j),this.i=null):this.l=Number.MAX_VALUE}if(c)for(b=0;b<c.length;b++)a=c[b],a<this.l?(a+=this.j,(d=this.h[a])?db(d):this.h[a]=hb):(ib(this),(d=this.i[a])?db(d):this.i[a]=hb)}
|
||||||
|
var hb=Object.freeze(db([]));function ib(a){var b=a.l+a.j;a.h[b]||(a.i=a.h[b]={})}function W(a,b,c){return-1===b?null:(void 0===c?0:c)||b>=a.l?a.i?a.i[b]:void 0:a.h[b+a.j]}function jb(a,b){var c=void 0===c?!1:c;var d=W(a,b,c);null==d&&(d=hb);d===hb&&(d=db([]),X(a,b,d,c));return d}function kb(a){var b=jb(a,3);a.m||(a.m={});if(!a.m[3]){for(var c=0;c<b.length;c++)b[c]=+b[c];a.m[3]=!0}return b}function lb(a,b,c){a=W(a,b);return null==a?c:a}
|
||||||
|
function Y(a,b,c){a=W(a,b);a=null==a?a:+a;return null==a?void 0===c?0:c:a}function X(a,b,c,d){(void 0===d?0:d)||b>=a.l?(ib(a),a.i[b]=c):a.h[b+a.j]=c}function mb(a,b,c){if(-1===c)return null;a.g||(a.g={});if(!a.g[c]){var d=W(a,c,!1);d&&(a.g[c]=new b(d))}return a.g[c]}function nb(a,b){a.g||(a.g={});var c=a.g[1];if(!c){var d=jb(a,1);c=[];for(var e=0;e<d.length;e++)c[e]=new b(d[e]);a.g[1]=c}return c}function ob(a,b,c){var d=void 0===d?!1:d;a.g||(a.g={});var e=c?pb(c,!1):c;a.g[b]=c;X(a,b,e,d)}
|
||||||
|
function qb(a,b,c,d){var e=nb(a,c);b=b?b:new c;a=jb(a,1);void 0!=d?(e.splice(d,0,b),a.splice(d,0,pb(b,!1))):(e.push(b),a.push(pb(b,!1)))}V.prototype.toJSON=function(){var a=pb(this,!1);return cb(a,eb,Fa)};function pb(a,b){if(a.g)for(var c in a.g){var d=a.g[c];if(Array.isArray(d))for(var e=0;e<d.length;e++)d[e]&&pb(d[e],b);else d&&pb(d,b)}return a.h}V.prototype.toString=function(){return pb(this,!1).toString()};function rb(a,b){if(a=a.o){Za(b,b.g.end());for(var c=0;c<a.length;c++)Za(b,a[c])}}function sb(a,b){if(4==b.h)return!1;var c=b.m;Ta(b);b.N||(b=La(b.g.h,c,b.g.g),(c=a.o)?c.push(b):a.o=[b]);return!0};function tb(a){V.call(this,a,-1,ub)}M(tb,V);tb.prototype.getRows=function(){return W(this,1)};tb.prototype.getCols=function(){return W(this,2)};tb.prototype.getPackedDataList=function(){return kb(this)};tb.prototype.getLayout=function(){return lb(this,4,0)};function vb(a,b){for(;S(b);)switch(b.i){case 8:var c=b.g.i();X(a,1,c);break;case 16:c=b.g.i();X(a,2,c);break;case 29:case 26:Xa(b,a.getPackedDataList());break;case 32:c=Oa(b.g);X(a,4,c);break;default:if(!sb(a,b))return a}return a}var ub=[3];function Z(a,b){var c=void 0;return new (c||(c=Promise))(function(d,e){function g(k){try{h(b.next(k))}catch(l){e(l)}}function f(k){try{h(b["throw"](k))}catch(l){e(l)}}function h(k){k.done?d(k.value):(new c(function(l){l(k.value)})).then(g,f)}h((b=b.apply(a,void 0)).next())})};function wb(a){V.call(this,a)}M(wb,V);function xb(a,b){for(;S(b);)switch(b.i){case 8:var c=b.g.i();X(a,1,c);break;case 21:c=T(b);X(a,2,c);break;case 26:c=Va(b);X(a,3,c);break;case 34:c=Va(b);X(a,4,c);break;default:if(!sb(a,b))return a}return a};function yb(a){V.call(this,a,-1,zb)}M(yb,V);yb.prototype.addClassification=function(a,b){qb(this,a,wb,b);return this};var zb=[1];function Ab(a){V.call(this,a)}M(Ab,V);function Bb(a,b){for(;S(b);)switch(b.i){case 13:var c=T(b);X(a,1,c);break;case 21:c=T(b);X(a,2,c);break;case 29:c=T(b);X(a,3,c);break;case 37:c=T(b);X(a,4,c);break;case 45:c=T(b);X(a,5,c);break;default:if(!sb(a,b))return a}return a};function Cb(a){V.call(this,a,-1,Db)}M(Cb,V);function Eb(a){a:{var b=new Cb;for(a=new Sa(a);S(a);)switch(a.i){case 10:var c=Ua(a,new Ab,Bb);qb(b,c,Ab,void 0);break;default:if(!sb(b,a))break a}}return b}var Db=[1];function Fb(a){V.call(this,a)}M(Fb,V);function Gb(a){V.call(this,a,-1,Hb)}M(Gb,V);Gb.prototype.getVertexType=function(){return lb(this,1,0)};Gb.prototype.getPrimitiveType=function(){return lb(this,2,0)};Gb.prototype.getVertexBufferList=function(){return kb(this)};Gb.prototype.getIndexBufferList=function(){return jb(this,4)};
|
||||||
|
function Ib(a,b){for(;S(b);)switch(b.i){case 8:var c=Oa(b.g);X(a,1,c);break;case 16:c=Oa(b.g);X(a,2,c);break;case 29:case 26:Xa(b,a.getVertexBufferList());break;case 32:case 34:c=b;var d=a.getIndexBufferList();2==c.h?Wa(c,Ma.prototype.i,d):d.push(c.g.i());break;default:if(!sb(a,b))return a}return a}var Hb=[3,4];function Jb(a){V.call(this,a)}M(Jb,V);Jb.prototype.getMesh=function(){return mb(this,Gb,1)};Jb.prototype.getPoseTransformMatrix=function(){return mb(this,tb,2)};function Kb(a){a:{var b=new Jb;for(a=new Sa(a);S(a);)switch(a.i){case 10:var c=Ua(a,new Gb,Ib);ob(b,1,c);break;case 18:c=Ua(a,new tb,vb);ob(b,2,c);break;default:if(!sb(b,a))break a}}return b};function Lb(a,b,c){c=a.createShader(0===c?a.VERTEX_SHADER:a.FRAGMENT_SHADER);a.shaderSource(c,b);a.compileShader(c);if(!a.getShaderParameter(c,a.COMPILE_STATUS))throw Error("Could not compile WebGL shader.\n\n"+a.getShaderInfoLog(c));return c};function Mb(a){return nb(a,wb).map(function(b){return{index:lb(b,1,0),Y:Y(b,2),label:null!=W(b,3)?lb(b,3,""):void 0,displayName:null!=W(b,4)?lb(b,4,""):void 0}})};function Nb(a){return{x:Y(a,1),y:Y(a,2),z:Y(a,3),visibility:null!=W(a,4)?Y(a,4):void 0}};function Ob(a,b){this.h=a;this.g=b;this.l=0}
|
||||||
|
function Pb(a,b,c){Qb(a,b);if("function"===typeof a.g.canvas.transferToImageBitmap)return Promise.resolve(a.g.canvas.transferToImageBitmap());if(c)return Promise.resolve(a.g.canvas);if("function"===typeof createImageBitmap)return createImageBitmap(a.g.canvas);void 0===a.i&&(a.i=document.createElement("canvas"));return new Promise(function(d){a.i.height=a.g.canvas.height;a.i.width=a.g.canvas.width;a.i.getContext("2d",{}).drawImage(a.g.canvas,0,0,a.g.canvas.width,a.g.canvas.height);d(a.i)})}
|
||||||
|
function Qb(a,b){var c=a.g;if(void 0===a.m){var d=Lb(c,"\n attribute vec2 aVertex;\n attribute vec2 aTex;\n varying vec2 vTex;\n void main(void) {\n gl_Position = vec4(aVertex, 0.0, 1.0);\n vTex = aTex;\n }",0),e=Lb(c,"\n precision mediump float;\n varying vec2 vTex;\n uniform sampler2D sampler0;\n void main(){\n gl_FragColor = texture2D(sampler0, vTex);\n }",1),g=c.createProgram();c.attachShader(g,d);c.attachShader(g,e);c.linkProgram(g);if(!c.getProgramParameter(g,c.LINK_STATUS))throw Error("Could not compile WebGL program.\n\n"+
|
||||||
|
c.getProgramInfoLog(g));d=a.m=g;c.useProgram(d);e=c.getUniformLocation(d,"sampler0");a.j={I:c.getAttribLocation(d,"aVertex"),H:c.getAttribLocation(d,"aTex"),da:e};a.s=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.s);c.enableVertexAttribArray(a.j.I);c.vertexAttribPointer(a.j.I,2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([-1,-1,-1,1,1,1,1,-1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);a.o=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.o);c.enableVertexAttribArray(a.j.H);c.vertexAttribPointer(a.j.H,
|
||||||
|
2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([0,1,0,0,1,0,1,1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);c.uniform1i(e,0)}d=a.j;c.useProgram(a.m);c.canvas.width=b.width;c.canvas.height=b.height;c.viewport(0,0,b.width,b.height);c.activeTexture(c.TEXTURE0);a.h.bindTexture2d(b.glName);c.enableVertexAttribArray(d.I);c.bindBuffer(c.ARRAY_BUFFER,a.s);c.vertexAttribPointer(d.I,2,c.FLOAT,!1,0,0);c.enableVertexAttribArray(d.H);c.bindBuffer(c.ARRAY_BUFFER,a.o);c.vertexAttribPointer(d.H,
|
||||||
|
2,c.FLOAT,!1,0,0);c.bindFramebuffer(c.DRAW_FRAMEBUFFER?c.DRAW_FRAMEBUFFER:c.FRAMEBUFFER,null);c.clearColor(0,0,0,0);c.clear(c.COLOR_BUFFER_BIT);c.colorMask(!0,!0,!0,!0);c.drawArrays(c.TRIANGLE_FAN,0,4);c.disableVertexAttribArray(d.I);c.disableVertexAttribArray(d.H);c.bindBuffer(c.ARRAY_BUFFER,null);a.h.bindTexture2d(0)}function Rb(a){this.g=a};var Sb=new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,9,1,7,0,65,0,253,15,26,11]);function Tb(a,b){return b+a}function Ub(a,b){window[a]=b}function Vb(a){var b=document.createElement("script");b.setAttribute("src",a);b.setAttribute("crossorigin","anonymous");return new Promise(function(c){b.addEventListener("load",function(){c()},!1);b.addEventListener("error",function(){c()},!1);document.body.appendChild(b)})}
|
||||||
|
function Wb(){return Z(this,function b(){return O(b,function(c){switch(c.g){case 1:return c.m=2,N(c,WebAssembly.instantiate(Sb),4);case 4:c.g=3;c.m=0;break;case 2:return c.m=0,c.j=null,c.return(!1);case 3:return c.return(!0)}})})}
|
||||||
|
function Xb(a){this.g=a;this.listeners={};this.j={};this.F={};this.m={};this.s={};this.G=this.o=this.R=!0;this.C=Promise.resolve();this.P="";this.B={};this.locateFile=a&&a.locateFile||Tb;if("object"===typeof window)var b=window.location.pathname.toString().substring(0,window.location.pathname.toString().lastIndexOf("/"))+"/";else if("undefined"!==typeof location)b=location.pathname.toString().substring(0,location.pathname.toString().lastIndexOf("/"))+"/";else throw Error("solutions can only be loaded on a web page or in a web worker");
|
||||||
|
this.S=b;if(a.options){b=K(Object.keys(a.options));for(var c=b.next();!c.done;c=b.next()){c=c.value;var d=a.options[c].default;void 0!==d&&(this.j[c]="function"===typeof d?d():d)}}}v=Xb.prototype;v.close=function(){this.i&&this.i.delete();return Promise.resolve()};function Yb(a,b){return void 0===a.g.files?[]:"function"===typeof a.g.files?a.g.files(b):a.g.files}
|
||||||
|
function Zb(a){return Z(a,function c(){var d=this,e,g,f,h,k,l,n,u,w,r,y;return O(c,function(m){switch(m.g){case 1:e=d;if(!d.R)return m.return();g=Yb(d,d.j);return N(m,Wb(),2);case 2:f=m.h;if("object"===typeof window)return Ub("createMediapipeSolutionsWasm",{locateFile:d.locateFile}),Ub("createMediapipeSolutionsPackedAssets",{locateFile:d.locateFile}),l=g.filter(function(t){return void 0!==t.data}),n=g.filter(function(t){return void 0===t.data}),u=Promise.all(l.map(function(t){var x=$b(e,t.url);if(void 0!==
|
||||||
|
t.path){var z=t.path;x=x.then(function(E){e.overrideFile(z,E);return Promise.resolve(E)})}return x})),w=Promise.all(n.map(function(t){return void 0===t.simd||t.simd&&f||!t.simd&&!f?Vb(e.locateFile(t.url,e.S)):Promise.resolve()})).then(function(){return Z(e,function x(){var z,E,F=this;return O(x,function(I){if(1==I.g)return z=window.createMediapipeSolutionsWasm,E=window.createMediapipeSolutionsPackedAssets,N(I,z(E),2);F.h=I.h;I.g=0})})}),r=function(){return Z(e,function x(){var z=this;return O(x,function(E){z.g.graph&&
|
||||||
|
z.g.graph.url?E=N(E,$b(z,z.g.graph.url),0):(E.g=0,E=void 0);return E})})}(),N(m,Promise.all([w,u,r]),7);if("function"!==typeof importScripts)throw Error("solutions can only be loaded on a web page or in a web worker");h=g.filter(function(t){return void 0===t.simd||t.simd&&f||!t.simd&&!f}).map(function(t){return e.locateFile(t.url,e.S)});importScripts.apply(null,L(h));return N(m,createMediapipeSolutionsWasm(Module),6);case 6:d.h=m.h;d.l=new OffscreenCanvas(1,1);d.h.canvas=d.l;k=d.h.GL.createContext(d.l,
|
||||||
|
{antialias:!1,alpha:!1,ba:"undefined"!==typeof WebGL2RenderingContext?2:1});d.h.GL.makeContextCurrent(k);m.g=4;break;case 7:d.l=document.createElement("canvas");y=d.l.getContext("webgl2",{});if(!y&&(y=d.l.getContext("webgl",{}),!y))return alert("Failed to create WebGL canvas context when passing video frame."),m.return();d.D=y;d.h.canvas=d.l;d.h.createContext(d.l,!0,!0,{});case 4:d.i=new d.h.SolutionWasm,d.R=!1,m.g=0}})})}
|
||||||
|
function ac(a){return Z(a,function c(){var d=this,e,g,f,h,k,l,n,u;return O(c,function(w){if(1==w.g){if(d.g.graph&&d.g.graph.url&&d.P===d.g.graph.url)return w.return();d.o=!0;if(!d.g.graph||!d.g.graph.url){w.g=2;return}d.P=d.g.graph.url;return N(w,$b(d,d.g.graph.url),3)}2!=w.g&&(e=w.h,d.i.loadGraph(e));g=K(Object.keys(d.B));for(f=g.next();!f.done;f=g.next())h=f.value,d.i.overrideFile(h,d.B[h]);d.B={};if(d.g.listeners)for(k=K(d.g.listeners),l=k.next();!l.done;l=k.next())n=l.value,bc(d,n);u=d.j;d.j=
|
||||||
|
{};d.setOptions(u);w.g=0})})}v.reset=function(){return Z(this,function b(){var c=this;return O(b,function(d){c.i&&(c.i.reset(),c.m={},c.s={});d.g=0})})};
|
||||||
|
v.setOptions=function(a,b){var c=this;if(b=b||this.g.options){for(var d=[],e=[],g={},f=K(Object.keys(a)),h=f.next();!h.done;g={K:g.K,L:g.L},h=f.next()){var k=h.value;k in this.j&&this.j[k]===a[k]||(this.j[k]=a[k],h=b[k],void 0!==h&&(h.onChange&&(g.K=h.onChange,g.L=a[k],d.push(function(l){return function(){return Z(c,function u(){var w,r=this;return O(u,function(y){if(1==y.g)return N(y,l.K(l.L),2);w=y.h;!0===w&&(r.o=!0);y.g=0})})}}(g))),h.graphOptionXref&&(k={valueNumber:1===h.type?a[k]:0,valueBoolean:0===
|
||||||
|
h.type?a[k]:!1,valueString:2===h.type?a[k]:""},h=Object.assign(Object.assign(Object.assign({},{calculatorName:"",calculatorIndex:0}),h.graphOptionXref),k),e.push(h))))}if(0!==d.length||0!==e.length)this.o=!0,this.A=(void 0===this.A?[]:this.A).concat(e),this.u=(void 0===this.u?[]:this.u).concat(d)}};
|
||||||
|
function cc(a){return Z(a,function c(){var d=this,e,g,f,h,k,l,n;return O(c,function(u){switch(u.g){case 1:if(!d.o)return u.return();if(!d.u){u.g=2;break}e=K(d.u);g=e.next();case 3:if(g.done){u.g=5;break}f=g.value;return N(u,f(),4);case 4:g=e.next();u.g=3;break;case 5:d.u=void 0;case 2:if(d.A){h=new d.h.GraphOptionChangeRequestList;k=K(d.A);for(l=k.next();!l.done;l=k.next())n=l.value,h.push_back(n);d.i.changeOptions(h);h.delete();d.A=void 0}d.o=!1;u.g=0}})})}
|
||||||
|
v.initialize=function(){return Z(this,function b(){var c=this;return O(b,function(d){return 1==d.g?N(d,Zb(c),2):3!=d.g?N(d,ac(c),3):N(d,cc(c),0)})})};function $b(a,b){return Z(a,function d(){var e=this,g,f;return O(d,function(h){if(b in e.F)return h.return(e.F[b]);g=e.locateFile(b,"");f=fetch(g).then(function(k){return k.arrayBuffer()});e.F[b]=f;return h.return(f)})})}v.overrideFile=function(a,b){this.i?this.i.overrideFile(a,b):this.B[a]=b};v.clearOverriddenFiles=function(){this.B={};this.i&&this.i.clearOverriddenFiles()};
|
||||||
|
v.send=function(a,b){return Z(this,function d(){var e=this,g,f,h,k,l,n,u,w,r;return O(d,function(y){switch(y.g){case 1:if(!e.g.inputs)return y.return();g=1E3*(void 0===b||null===b?performance.now():b);return N(y,e.C,2);case 2:return N(y,e.initialize(),3);case 3:f=new e.h.PacketDataList;h=K(Object.keys(a));for(k=h.next();!k.done;k=h.next())if(l=k.value,n=e.g.inputs[l]){a:{var m=e;var t=a[l];switch(n.type){case "video":var x=m.m[n.stream];x||(x=new Ob(m.h,m.D),m.m[n.stream]=x);m=x;0===m.l&&(m.l=m.h.createTexture());
|
||||||
|
if("undefined"!==typeof HTMLVideoElement&&t instanceof HTMLVideoElement){var z=t.videoWidth;x=t.videoHeight}else"undefined"!==typeof HTMLImageElement&&t instanceof HTMLImageElement?(z=t.naturalWidth,x=t.naturalHeight):(z=t.width,x=t.height);x={glName:m.l,width:z,height:x};z=m.g;z.canvas.width=x.width;z.canvas.height=x.height;z.activeTexture(z.TEXTURE0);m.h.bindTexture2d(m.l);z.texImage2D(z.TEXTURE_2D,0,z.RGBA,z.RGBA,z.UNSIGNED_BYTE,t);m.h.bindTexture2d(0);m=x;break a;case "detections":x=m.m[n.stream];
|
||||||
|
x||(x=new Rb(m.h),m.m[n.stream]=x);m=x;m.data||(m.data=new m.g.DetectionListData);m.data.reset(t.length);for(x=0;x<t.length;++x){z=t[x];var E=m.data,F=E.setBoundingBox,I=x;var H=z.T;var p=new Fb;X(p,1,H.Z);X(p,2,H.$);X(p,3,H.height);X(p,4,H.width);X(p,5,H.rotation);X(p,6,H.X);var A=H=new Ya;U(A,1,W(p,1));U(A,2,W(p,2));U(A,3,W(p,3));U(A,4,W(p,4));U(A,5,W(p,5));var C=W(p,6);if(null!=C&&null!=C){Ra(A.g,48);var q=A.g,B=C;C=0>B;B=Math.abs(B);var D=B>>>0;B=Math.floor((B-D)/4294967296);B>>>=0;C&&(B=~B>>>
|
||||||
|
0,D=(~D>>>0)+1,4294967295<D&&(D=0,B++,4294967295<B&&(B=0)));Q=D;R=B;C=Q;for(D=R;0<D||127<C;)q.push(C&127|128),C=(C>>>7|D<<25)>>>0,D>>>=7;q.push(C)}rb(p,A);H=$a(H);F.call(E,I,H);if(z.O)for(E=0;E<z.O.length;++E)p=z.O[E],A=p.visibility?!0:!1,F=m.data,I=F.addNormalizedLandmark,H=x,p=Object.assign(Object.assign({},p),{visibility:A?p.visibility:0}),A=new Ab,X(A,1,p.x),X(A,2,p.y),X(A,3,p.z),p.visibility&&X(A,4,p.visibility),q=p=new Ya,U(q,1,W(A,1)),U(q,2,W(A,2)),U(q,3,W(A,3)),U(q,4,W(A,4)),U(q,5,W(A,5)),
|
||||||
|
rb(A,q),p=$a(p),I.call(F,H,p);if(z.M)for(E=0;E<z.M.length;++E){F=m.data;I=F.addClassification;H=x;p=z.M[E];A=new wb;X(A,2,p.Y);p.index&&X(A,1,p.index);p.label&&X(A,3,p.label);p.displayName&&X(A,4,p.displayName);q=p=new Ya;D=W(A,1);if(null!=D&&null!=D)if(Ra(q.g,8),C=q.g,0<=D)Ra(C,D);else{for(B=0;9>B;B++)C.push(D&127|128),D>>=7;C.push(1)}U(q,2,W(A,2));C=W(A,3);null!=C&&(C=Ca(C),Ra(q.g,26),Ra(q.g,C.length),Za(q,q.g.end()),Za(q,C));C=W(A,4);null!=C&&(C=Ca(C),Ra(q.g,34),Ra(q.g,C.length),Za(q,q.g.end()),
|
||||||
|
Za(q,C));rb(A,q);p=$a(p);I.call(F,H,p)}}m=m.data;break a;default:m={}}}u=m;w=n.stream;switch(n.type){case "video":f.pushTexture2d(Object.assign(Object.assign({},u),{stream:w,timestamp:g}));break;case "detections":r=u;r.stream=w;r.timestamp=g;f.pushDetectionList(r);break;default:throw Error("Unknown input config type: '"+n.type+"'");}}e.i.send(f);return N(y,e.C,4);case 4:f.delete(),y.g=0}})})};
|
||||||
|
function dc(a,b,c){return Z(a,function e(){var g,f,h,k,l,n,u=this,w,r,y,m,t,x,z,E;return O(e,function(F){switch(F.g){case 1:if(!c)return F.return(b);g={};f=0;h=K(Object.keys(c));for(k=h.next();!k.done;k=h.next())l=k.value,n=c[l],"string"!==typeof n&&"texture"===n.type&&void 0!==b[n.stream]&&++f;1<f&&(u.G=!1);w=K(Object.keys(c));k=w.next();case 2:if(k.done){F.g=4;break}r=k.value;y=c[r];if("string"===typeof y)return z=g,E=r,N(F,ec(u,r,b[y]),14);m=b[y.stream];if("detection_list"===y.type){if(m){var I=
|
||||||
|
m.getRectList();for(var H=m.getLandmarksList(),p=m.getClassificationsList(),A=[],C=0;C<I.size();++C){var q=I.get(C);a:{var B=new Fb;for(q=new Sa(q);S(q);)switch(q.i){case 13:var D=T(q);X(B,1,D);break;case 21:D=T(q);X(B,2,D);break;case 29:D=T(q);X(B,3,D);break;case 37:D=T(q);X(B,4,D);break;case 45:D=T(q);X(B,5,D);break;case 48:D=Oa(q.g);X(B,6,D);break;default:if(!sb(B,q))break a}}B={Z:Y(B,1),$:Y(B,2),height:Y(B,3),width:Y(B,4),rotation:Y(B,5,0),X:lb(B,6,0)};q=nb(Eb(H.get(C)),Ab).map(Nb);var la=p.get(C);
|
||||||
|
a:for(D=new yb,la=new Sa(la);S(la);)switch(la.i){case 10:D.addClassification(Ua(la,new wb,xb));break;default:if(!sb(D,la))break a}B={T:B,O:q,M:Mb(D)};A.push(B)}I=A}else I=[];g[r]=I;F.g=7;break}if("proto_list"===y.type){if(m){I=Array(m.size());for(H=0;H<m.size();H++)I[H]=m.get(H);m.delete()}else I=[];g[r]=I;F.g=7;break}if(void 0===m){F.g=3;break}if("float_list"===y.type){g[r]=m;F.g=7;break}if("proto"===y.type){g[r]=m;F.g=7;break}if("texture"!==y.type)throw Error("Unknown output config type: '"+y.type+
|
||||||
|
"'");t=u.s[r];t||(t=new Ob(u.h,u.D),u.s[r]=t);return N(F,Pb(t,m,u.G),13);case 13:x=F.h,g[r]=x;case 7:y.transform&&g[r]&&(g[r]=y.transform(g[r]));F.g=3;break;case 14:z[E]=F.h;case 3:k=w.next();F.g=2;break;case 4:return F.return(g)}})})}
|
||||||
|
function ec(a,b,c){return Z(a,function e(){var g=this,f;return O(e,function(h){return"number"===typeof c||c instanceof Uint8Array||c instanceof g.h.Uint8BlobList?h.return(c):c instanceof g.h.Texture2dDataOut?(f=g.s[b],f||(f=new Ob(g.h,g.D),g.s[b]=f),h.return(Pb(f,c,g.G))):h.return(void 0)})})}
|
||||||
|
function bc(a,b){for(var c=b.name||"$",d=[].concat(L(b.wants)),e=new a.h.StringList,g=K(b.wants),f=g.next();!f.done;f=g.next())e.push_back(f.value);g=a.h.PacketListener.implement({onResults:function(h){for(var k={},l=0;l<b.wants.length;++l)k[d[l]]=h.get(l);var n=a.listeners[c];n&&(a.C=dc(a,k,b.outs).then(function(u){u=n(u);for(var w=0;w<b.wants.length;++w){var r=k[d[w]];"object"===typeof r&&r.hasOwnProperty&&r.hasOwnProperty("delete")&&r.delete()}u&&(a.C=u)}))}});a.i.attachMultiListener(e,g);e.delete()}
|
||||||
|
v.onResults=function(a,b){this.listeners[b||"$"]=a};P("Solution",Xb);P("OptionType",{BOOL:0,NUMBER:1,aa:2,0:"BOOL",1:"NUMBER",2:"STRING"});function fc(a){a=Kb(a);var b=a.getMesh();if(!b)return a;var c=new Float32Array(b.getVertexBufferList());b.getVertexBufferList=function(){return c};var d=new Uint32Array(b.getIndexBufferList());b.getIndexBufferList=function(){return d};return a};var gc={files:[{url:"face_mesh_solution_packed_assets_loader.js"},{simd:!0,url:"face_mesh_solution_simd_wasm_bin.js"},{simd:!1,url:"face_mesh_solution_wasm_bin.js"}],graph:{url:"face_mesh.binarypb"},listeners:[{wants:["multi_face_geometry","image_transformed","multi_face_landmarks"],outs:{image:"image_transformed",multiFaceGeometry:{type:"proto_list",stream:"multi_face_geometry",transform:function(a){return a.map(fc)}},multiFaceLandmarks:{type:"proto_list",stream:"multi_face_landmarks",transform:function(a){return a.map(function(b){return nb(Eb(b),
|
||||||
|
Ab).map(Nb)})}}}}],inputs:{image:{type:"video",stream:"input_frames_gpu"}},options:{useCpuInference:{type:0,graphOptionXref:{calculatorType:"InferenceCalculator",fieldName:"use_cpu_inference"},default:"iPad Simulator;iPhone Simulator;iPod Simulator;iPad;iPhone;iPod".split(";").includes(navigator.platform)||navigator.userAgent.includes("Mac")&&"ontouchend"in document},enableFaceGeometry:{type:0,graphOptionXref:{calculatorName:"EnableFaceGeometryConstant",calculatorType:"ConstantSidePacketCalculator",
|
||||||
|
fieldName:"bool_value"}},selfieMode:{type:0,graphOptionXref:{calculatorType:"GlScalerCalculator",calculatorIndex:1,fieldName:"flip_horizontal"}},maxNumFaces:{type:1,graphOptionXref:{calculatorType:"ConstantSidePacketCalculator",calculatorName:"ConstantSidePacketCalculatorNumFaces",fieldName:"int_value"}},refineLandmarks:{type:0,graphOptionXref:{calculatorType:"ConstantSidePacketCalculator",calculatorName:"ConstantSidePacketCalculatorRefineLandmarks",fieldName:"bool_value"}},minDetectionConfidence:{type:1,
|
||||||
|
graphOptionXref:{calculatorType:"TensorsToDetectionsCalculator",calculatorName:"facelandmarkfrontgpu__facedetectionshortrangegpu__facedetectionshortrangecommon__TensorsToDetectionsCalculator",fieldName:"min_score_thresh"}},minTrackingConfidence:{type:1,graphOptionXref:{calculatorType:"ThresholdingCalculator",calculatorName:"facelandmarkfrontgpu__facelandmarkgpu__ThresholdingCalculator",fieldName:"threshold"}},cameraNear:{type:1,graphOptionXref:{calculatorType:"FaceGeometryEnvGeneratorCalculator",
|
||||||
|
fieldName:"near"}},cameraFar:{type:1,graphOptionXref:{calculatorType:"FaceGeometryEnvGeneratorCalculator",fieldName:"far"}},cameraVerticalFovDegrees:{type:1,graphOptionXref:{calculatorType:"FaceGeometryEnvGeneratorCalculator",fieldName:"vertical_fov_degrees"}}}};var hc=[[61,146],[146,91],[91,181],[181,84],[84,17],[17,314],[314,405],[405,321],[321,375],[375,291],[61,185],[185,40],[40,39],[39,37],[37,0],[0,267],[267,269],[269,270],[270,409],[409,291],[78,95],[95,88],[88,178],[178,87],[87,14],[14,317],[317,402],[402,318],[318,324],[324,308],[78,191],[191,80],[80,81],[81,82],[82,13],[13,312],[312,311],[311,310],[310,415],[415,308]],ic=[[263,249],[249,390],[390,373],[373,374],[374,380],[380,381],[381,382],[382,362],[263,466],[466,388],[388,387],[387,386],[386,
|
||||||
|
385],[385,384],[384,398],[398,362]],jc=[[276,283],[283,282],[282,295],[295,285],[300,293],[293,334],[334,296],[296,336]],kc=[[33,7],[7,163],[163,144],[144,145],[145,153],[153,154],[154,155],[155,133],[33,246],[246,161],[161,160],[160,159],[159,158],[158,157],[157,173],[173,133]],lc=[[46,53],[53,52],[52,65],[65,55],[70,63],[63,105],[105,66],[66,107]],mc=[[10,338],[338,297],[297,332],[332,284],[284,251],[251,389],[389,356],[356,454],[454,323],[323,361],[361,288],[288,397],[397,365],[365,379],[379,378],
|
||||||
|
[378,400],[400,377],[377,152],[152,148],[148,176],[176,149],[149,150],[150,136],[136,172],[172,58],[58,132],[132,93],[93,234],[234,127],[127,162],[162,21],[21,54],[54,103],[103,67],[67,109],[109,10]],nc=[].concat(L(hc),L(ic),L(jc),L(kc),L(lc),L(mc));function oc(a){a=a||{};a=Object.assign(Object.assign({},gc),a);this.g=new Xb(a)}v=oc.prototype;v.close=function(){this.g.close();return Promise.resolve()};v.onResults=function(a){this.g.onResults(a)};v.initialize=function(){return Z(this,function b(){var c=this;return O(b,function(d){return N(d,c.g.initialize(),0)})})};v.reset=function(){this.g.reset()};v.send=function(a){return Z(this,function c(){var d=this;return O(c,function(e){return N(e,d.g.send(a),0)})})};v.setOptions=function(a){this.g.setOptions(a)};
|
||||||
|
P("FACE_GEOMETRY",{Layout:{COLUMN_MAJOR:0,ROW_MAJOR:1,0:"COLUMN_MAJOR",1:"ROW_MAJOR"},PrimitiveType:{TRIANGLE:0,0:"TRIANGLE"},VertexType:{VERTEX_PT:0,0:"VERTEX_PT"},DEFAULT_CAMERA_PARAMS:{verticalFovDegrees:63,near:1,far:1E4}});P("FaceMesh",oc);P("FACEMESH_LIPS",hc);P("FACEMESH_LEFT_EYE",ic);P("FACEMESH_LEFT_EYEBROW",jc);P("FACEMESH_LEFT_IRIS",[[474,475],[475,476],[476,477],[477,474]]);P("FACEMESH_RIGHT_EYE",kc);P("FACEMESH_RIGHT_EYEBROW",lc);
|
||||||
|
P("FACEMESH_RIGHT_IRIS",[[469,470],[470,471],[471,472],[472,469]]);P("FACEMESH_FACE_OVAL",mc);P("FACEMESH_CONTOURS",nc);
|
||||||
|
P("FACEMESH_TESSELATION",[[127,34],[34,139],[139,127],[11,0],[0,37],[37,11],[232,231],[231,120],[120,232],[72,37],[37,39],[39,72],[128,121],[121,47],[47,128],[232,121],[121,128],[128,232],[104,69],[69,67],[67,104],[175,171],[171,148],[148,175],[118,50],[50,101],[101,118],[73,39],[39,40],[40,73],[9,151],[151,108],[108,9],[48,115],[115,131],[131,48],[194,204],[204,211],[211,194],[74,40],[40,185],[185,74],[80,42],[42,183],[183,80],[40,92],[92,186],[186,40],[230,229],[229,118],[118,230],[202,212],[212,
|
||||||
|
214],[214,202],[83,18],[18,17],[17,83],[76,61],[61,146],[146,76],[160,29],[29,30],[30,160],[56,157],[157,173],[173,56],[106,204],[204,194],[194,106],[135,214],[214,192],[192,135],[203,165],[165,98],[98,203],[21,71],[71,68],[68,21],[51,45],[45,4],[4,51],[144,24],[24,23],[23,144],[77,146],[146,91],[91,77],[205,50],[50,187],[187,205],[201,200],[200,18],[18,201],[91,106],[106,182],[182,91],[90,91],[91,181],[181,90],[85,84],[84,17],[17,85],[206,203],[203,36],[36,206],[148,171],[171,140],[140,148],[92,
|
||||||
|
40],[40,39],[39,92],[193,189],[189,244],[244,193],[159,158],[158,28],[28,159],[247,246],[246,161],[161,247],[236,3],[3,196],[196,236],[54,68],[68,104],[104,54],[193,168],[168,8],[8,193],[117,228],[228,31],[31,117],[189,193],[193,55],[55,189],[98,97],[97,99],[99,98],[126,47],[47,100],[100,126],[166,79],[79,218],[218,166],[155,154],[154,26],[26,155],[209,49],[49,131],[131,209],[135,136],[136,150],[150,135],[47,126],[126,217],[217,47],[223,52],[52,53],[53,223],[45,51],[51,134],[134,45],[211,170],[170,
|
||||||
|
140],[140,211],[67,69],[69,108],[108,67],[43,106],[106,91],[91,43],[230,119],[119,120],[120,230],[226,130],[130,247],[247,226],[63,53],[53,52],[52,63],[238,20],[20,242],[242,238],[46,70],[70,156],[156,46],[78,62],[62,96],[96,78],[46,53],[53,63],[63,46],[143,34],[34,227],[227,143],[123,117],[117,111],[111,123],[44,125],[125,19],[19,44],[236,134],[134,51],[51,236],[216,206],[206,205],[205,216],[154,153],[153,22],[22,154],[39,37],[37,167],[167,39],[200,201],[201,208],[208,200],[36,142],[142,100],[100,
|
||||||
|
36],[57,212],[212,202],[202,57],[20,60],[60,99],[99,20],[28,158],[158,157],[157,28],[35,226],[226,113],[113,35],[160,159],[159,27],[27,160],[204,202],[202,210],[210,204],[113,225],[225,46],[46,113],[43,202],[202,204],[204,43],[62,76],[76,77],[77,62],[137,123],[123,116],[116,137],[41,38],[38,72],[72,41],[203,129],[129,142],[142,203],[64,98],[98,240],[240,64],[49,102],[102,64],[64,49],[41,73],[73,74],[74,41],[212,216],[216,207],[207,212],[42,74],[74,184],[184,42],[169,170],[170,211],[211,169],[170,
|
||||||
|
149],[149,176],[176,170],[105,66],[66,69],[69,105],[122,6],[6,168],[168,122],[123,147],[147,187],[187,123],[96,77],[77,90],[90,96],[65,55],[55,107],[107,65],[89,90],[90,180],[180,89],[101,100],[100,120],[120,101],[63,105],[105,104],[104,63],[93,137],[137,227],[227,93],[15,86],[86,85],[85,15],[129,102],[102,49],[49,129],[14,87],[87,86],[86,14],[55,8],[8,9],[9,55],[100,47],[47,121],[121,100],[145,23],[23,22],[22,145],[88,89],[89,179],[179,88],[6,122],[122,196],[196,6],[88,95],[95,96],[96,88],[138,172],
|
||||||
|
[172,136],[136,138],[215,58],[58,172],[172,215],[115,48],[48,219],[219,115],[42,80],[80,81],[81,42],[195,3],[3,51],[51,195],[43,146],[146,61],[61,43],[171,175],[175,199],[199,171],[81,82],[82,38],[38,81],[53,46],[46,225],[225,53],[144,163],[163,110],[110,144],[52,65],[65,66],[66,52],[229,228],[228,117],[117,229],[34,127],[127,234],[234,34],[107,108],[108,69],[69,107],[109,108],[108,151],[151,109],[48,64],[64,235],[235,48],[62,78],[78,191],[191,62],[129,209],[209,126],[126,129],[111,35],[35,143],[143,
|
||||||
|
111],[117,123],[123,50],[50,117],[222,65],[65,52],[52,222],[19,125],[125,141],[141,19],[221,55],[55,65],[65,221],[3,195],[195,197],[197,3],[25,7],[7,33],[33,25],[220,237],[237,44],[44,220],[70,71],[71,139],[139,70],[122,193],[193,245],[245,122],[247,130],[130,33],[33,247],[71,21],[21,162],[162,71],[170,169],[169,150],[150,170],[188,174],[174,196],[196,188],[216,186],[186,92],[92,216],[2,97],[97,167],[167,2],[141,125],[125,241],[241,141],[164,167],[167,37],[37,164],[72,38],[38,12],[12,72],[38,82],
|
||||||
|
[82,13],[13,38],[63,68],[68,71],[71,63],[226,35],[35,111],[111,226],[101,50],[50,205],[205,101],[206,92],[92,165],[165,206],[209,198],[198,217],[217,209],[165,167],[167,97],[97,165],[220,115],[115,218],[218,220],[133,112],[112,243],[243,133],[239,238],[238,241],[241,239],[214,135],[135,169],[169,214],[190,173],[173,133],[133,190],[171,208],[208,32],[32,171],[125,44],[44,237],[237,125],[86,87],[87,178],[178,86],[85,86],[86,179],[179,85],[84,85],[85,180],[180,84],[83,84],[84,181],[181,83],[201,83],
|
||||||
|
[83,182],[182,201],[137,93],[93,132],[132,137],[76,62],[62,183],[183,76],[61,76],[76,184],[184,61],[57,61],[61,185],[185,57],[212,57],[57,186],[186,212],[214,207],[207,187],[187,214],[34,143],[143,156],[156,34],[79,239],[239,237],[237,79],[123,137],[137,177],[177,123],[44,1],[1,4],[4,44],[201,194],[194,32],[32,201],[64,102],[102,129],[129,64],[213,215],[215,138],[138,213],[59,166],[166,219],[219,59],[242,99],[99,97],[97,242],[2,94],[94,141],[141,2],[75,59],[59,235],[235,75],[24,110],[110,228],[228,
|
||||||
|
24],[25,130],[130,226],[226,25],[23,24],[24,229],[229,23],[22,23],[23,230],[230,22],[26,22],[22,231],[231,26],[112,26],[26,232],[232,112],[189,190],[190,243],[243,189],[221,56],[56,190],[190,221],[28,56],[56,221],[221,28],[27,28],[28,222],[222,27],[29,27],[27,223],[223,29],[30,29],[29,224],[224,30],[247,30],[30,225],[225,247],[238,79],[79,20],[20,238],[166,59],[59,75],[75,166],[60,75],[75,240],[240,60],[147,177],[177,215],[215,147],[20,79],[79,166],[166,20],[187,147],[147,213],[213,187],[112,233],
|
||||||
|
[233,244],[244,112],[233,128],[128,245],[245,233],[128,114],[114,188],[188,128],[114,217],[217,174],[174,114],[131,115],[115,220],[220,131],[217,198],[198,236],[236,217],[198,131],[131,134],[134,198],[177,132],[132,58],[58,177],[143,35],[35,124],[124,143],[110,163],[163,7],[7,110],[228,110],[110,25],[25,228],[356,389],[389,368],[368,356],[11,302],[302,267],[267,11],[452,350],[350,349],[349,452],[302,303],[303,269],[269,302],[357,343],[343,277],[277,357],[452,453],[453,357],[357,452],[333,332],[332,
|
||||||
|
297],[297,333],[175,152],[152,377],[377,175],[347,348],[348,330],[330,347],[303,304],[304,270],[270,303],[9,336],[336,337],[337,9],[278,279],[279,360],[360,278],[418,262],[262,431],[431,418],[304,408],[408,409],[409,304],[310,415],[415,407],[407,310],[270,409],[409,410],[410,270],[450,348],[348,347],[347,450],[422,430],[430,434],[434,422],[313,314],[314,17],[17,313],[306,307],[307,375],[375,306],[387,388],[388,260],[260,387],[286,414],[414,398],[398,286],[335,406],[406,418],[418,335],[364,367],[367,
|
||||||
|
416],[416,364],[423,358],[358,327],[327,423],[251,284],[284,298],[298,251],[281,5],[5,4],[4,281],[373,374],[374,253],[253,373],[307,320],[320,321],[321,307],[425,427],[427,411],[411,425],[421,313],[313,18],[18,421],[321,405],[405,406],[406,321],[320,404],[404,405],[405,320],[315,16],[16,17],[17,315],[426,425],[425,266],[266,426],[377,400],[400,369],[369,377],[322,391],[391,269],[269,322],[417,465],[465,464],[464,417],[386,257],[257,258],[258,386],[466,260],[260,388],[388,466],[456,399],[399,419],
|
||||||
|
[419,456],[284,332],[332,333],[333,284],[417,285],[285,8],[8,417],[346,340],[340,261],[261,346],[413,441],[441,285],[285,413],[327,460],[460,328],[328,327],[355,371],[371,329],[329,355],[392,439],[439,438],[438,392],[382,341],[341,256],[256,382],[429,420],[420,360],[360,429],[364,394],[394,379],[379,364],[277,343],[343,437],[437,277],[443,444],[444,283],[283,443],[275,440],[440,363],[363,275],[431,262],[262,369],[369,431],[297,338],[338,337],[337,297],[273,375],[375,321],[321,273],[450,451],[451,
|
||||||
|
349],[349,450],[446,342],[342,467],[467,446],[293,334],[334,282],[282,293],[458,461],[461,462],[462,458],[276,353],[353,383],[383,276],[308,324],[324,325],[325,308],[276,300],[300,293],[293,276],[372,345],[345,447],[447,372],[352,345],[345,340],[340,352],[274,1],[1,19],[19,274],[456,248],[248,281],[281,456],[436,427],[427,425],[425,436],[381,256],[256,252],[252,381],[269,391],[391,393],[393,269],[200,199],[199,428],[428,200],[266,330],[330,329],[329,266],[287,273],[273,422],[422,287],[250,462],[462,
|
||||||
|
328],[328,250],[258,286],[286,384],[384,258],[265,353],[353,342],[342,265],[387,259],[259,257],[257,387],[424,431],[431,430],[430,424],[342,353],[353,276],[276,342],[273,335],[335,424],[424,273],[292,325],[325,307],[307,292],[366,447],[447,345],[345,366],[271,303],[303,302],[302,271],[423,266],[266,371],[371,423],[294,455],[455,460],[460,294],[279,278],[278,294],[294,279],[271,272],[272,304],[304,271],[432,434],[434,427],[427,432],[272,407],[407,408],[408,272],[394,430],[430,431],[431,394],[395,369],
|
||||||
|
[369,400],[400,395],[334,333],[333,299],[299,334],[351,417],[417,168],[168,351],[352,280],[280,411],[411,352],[325,319],[319,320],[320,325],[295,296],[296,336],[336,295],[319,403],[403,404],[404,319],[330,348],[348,349],[349,330],[293,298],[298,333],[333,293],[323,454],[454,447],[447,323],[15,16],[16,315],[315,15],[358,429],[429,279],[279,358],[14,15],[15,316],[316,14],[285,336],[336,9],[9,285],[329,349],[349,350],[350,329],[374,380],[380,252],[252,374],[318,402],[402,403],[403,318],[6,197],[197,
|
||||||
|
419],[419,6],[318,319],[319,325],[325,318],[367,364],[364,365],[365,367],[435,367],[367,397],[397,435],[344,438],[438,439],[439,344],[272,271],[271,311],[311,272],[195,5],[5,281],[281,195],[273,287],[287,291],[291,273],[396,428],[428,199],[199,396],[311,271],[271,268],[268,311],[283,444],[444,445],[445,283],[373,254],[254,339],[339,373],[282,334],[334,296],[296,282],[449,347],[347,346],[346,449],[264,447],[447,454],[454,264],[336,296],[296,299],[299,336],[338,10],[10,151],[151,338],[278,439],[439,
|
||||||
|
455],[455,278],[292,407],[407,415],[415,292],[358,371],[371,355],[355,358],[340,345],[345,372],[372,340],[346,347],[347,280],[280,346],[442,443],[443,282],[282,442],[19,94],[94,370],[370,19],[441,442],[442,295],[295,441],[248,419],[419,197],[197,248],[263,255],[255,359],[359,263],[440,275],[275,274],[274,440],[300,383],[383,368],[368,300],[351,412],[412,465],[465,351],[263,467],[467,466],[466,263],[301,368],[368,389],[389,301],[395,378],[378,379],[379,395],[412,351],[351,419],[419,412],[436,426],
|
||||||
|
[426,322],[322,436],[2,164],[164,393],[393,2],[370,462],[462,461],[461,370],[164,0],[0,267],[267,164],[302,11],[11,12],[12,302],[268,12],[12,13],[13,268],[293,300],[300,301],[301,293],[446,261],[261,340],[340,446],[330,266],[266,425],[425,330],[426,423],[423,391],[391,426],[429,355],[355,437],[437,429],[391,327],[327,326],[326,391],[440,457],[457,438],[438,440],[341,382],[382,362],[362,341],[459,457],[457,461],[461,459],[434,430],[430,394],[394,434],[414,463],[463,362],[362,414],[396,369],[369,262],
|
||||||
|
[262,396],[354,461],[461,457],[457,354],[316,403],[403,402],[402,316],[315,404],[404,403],[403,315],[314,405],[405,404],[404,314],[313,406],[406,405],[405,313],[421,418],[418,406],[406,421],[366,401],[401,361],[361,366],[306,408],[408,407],[407,306],[291,409],[409,408],[408,291],[287,410],[410,409],[409,287],[432,436],[436,410],[410,432],[434,416],[416,411],[411,434],[264,368],[368,383],[383,264],[309,438],[438,457],[457,309],[352,376],[376,401],[401,352],[274,275],[275,4],[4,274],[421,428],[428,
|
||||||
|
262],[262,421],[294,327],[327,358],[358,294],[433,416],[416,367],[367,433],[289,455],[455,439],[439,289],[462,370],[370,326],[326,462],[2,326],[326,370],[370,2],[305,460],[460,455],[455,305],[254,449],[449,448],[448,254],[255,261],[261,446],[446,255],[253,450],[450,449],[449,253],[252,451],[451,450],[450,252],[256,452],[452,451],[451,256],[341,453],[453,452],[452,341],[413,464],[464,463],[463,413],[441,413],[413,414],[414,441],[258,442],[442,441],[441,258],[257,443],[443,442],[442,257],[259,444],
|
||||||
|
[444,443],[443,259],[260,445],[445,444],[444,260],[467,342],[342,445],[445,467],[459,458],[458,250],[250,459],[289,392],[392,290],[290,289],[290,328],[328,460],[460,290],[376,433],[433,435],[435,376],[250,290],[290,392],[392,250],[411,416],[416,433],[433,411],[341,463],[463,464],[464,341],[453,464],[464,465],[465,453],[357,465],[465,412],[412,357],[343,412],[412,399],[399,343],[360,363],[363,440],[440,360],[437,399],[399,456],[456,437],[420,456],[456,363],[363,420],[401,435],[435,288],[288,401],[372,
|
||||||
|
383],[383,353],[353,372],[339,255],[255,249],[249,339],[448,261],[261,255],[255,448],[133,243],[243,190],[190,133],[133,155],[155,112],[112,133],[33,246],[246,247],[247,33],[33,130],[130,25],[25,33],[398,384],[384,286],[286,398],[362,398],[398,414],[414,362],[362,463],[463,341],[341,362],[263,359],[359,467],[467,263],[263,249],[249,255],[255,263],[466,467],[467,260],[260,466],[75,60],[60,166],[166,75],[238,239],[239,79],[79,238],[162,127],[127,139],[139,162],[72,11],[11,37],[37,72],[121,232],[232,
|
||||||
|
120],[120,121],[73,72],[72,39],[39,73],[114,128],[128,47],[47,114],[233,232],[232,128],[128,233],[103,104],[104,67],[67,103],[152,175],[175,148],[148,152],[119,118],[118,101],[101,119],[74,73],[73,40],[40,74],[107,9],[9,108],[108,107],[49,48],[48,131],[131,49],[32,194],[194,211],[211,32],[184,74],[74,185],[185,184],[191,80],[80,183],[183,191],[185,40],[40,186],[186,185],[119,230],[230,118],[118,119],[210,202],[202,214],[214,210],[84,83],[83,17],[17,84],[77,76],[76,146],[146,77],[161,160],[160,30],
|
||||||
|
[30,161],[190,56],[56,173],[173,190],[182,106],[106,194],[194,182],[138,135],[135,192],[192,138],[129,203],[203,98],[98,129],[54,21],[21,68],[68,54],[5,51],[51,4],[4,5],[145,144],[144,23],[23,145],[90,77],[77,91],[91,90],[207,205],[205,187],[187,207],[83,201],[201,18],[18,83],[181,91],[91,182],[182,181],[180,90],[90,181],[181,180],[16,85],[85,17],[17,16],[205,206],[206,36],[36,205],[176,148],[148,140],[140,176],[165,92],[92,39],[39,165],[245,193],[193,244],[244,245],[27,159],[159,28],[28,27],[30,
|
||||||
|
247],[247,161],[161,30],[174,236],[236,196],[196,174],[103,54],[54,104],[104,103],[55,193],[193,8],[8,55],[111,117],[117,31],[31,111],[221,189],[189,55],[55,221],[240,98],[98,99],[99,240],[142,126],[126,100],[100,142],[219,166],[166,218],[218,219],[112,155],[155,26],[26,112],[198,209],[209,131],[131,198],[169,135],[135,150],[150,169],[114,47],[47,217],[217,114],[224,223],[223,53],[53,224],[220,45],[45,134],[134,220],[32,211],[211,140],[140,32],[109,67],[67,108],[108,109],[146,43],[43,91],[91,146],
|
||||||
|
[231,230],[230,120],[120,231],[113,226],[226,247],[247,113],[105,63],[63,52],[52,105],[241,238],[238,242],[242,241],[124,46],[46,156],[156,124],[95,78],[78,96],[96,95],[70,46],[46,63],[63,70],[116,143],[143,227],[227,116],[116,123],[123,111],[111,116],[1,44],[44,19],[19,1],[3,236],[236,51],[51,3],[207,216],[216,205],[205,207],[26,154],[154,22],[22,26],[165,39],[39,167],[167,165],[199,200],[200,208],[208,199],[101,36],[36,100],[100,101],[43,57],[57,202],[202,43],[242,20],[20,99],[99,242],[56,28],[28,
|
||||||
|
157],[157,56],[124,35],[35,113],[113,124],[29,160],[160,27],[27,29],[211,204],[204,210],[210,211],[124,113],[113,46],[46,124],[106,43],[43,204],[204,106],[96,62],[62,77],[77,96],[227,137],[137,116],[116,227],[73,41],[41,72],[72,73],[36,203],[203,142],[142,36],[235,64],[64,240],[240,235],[48,49],[49,64],[64,48],[42,41],[41,74],[74,42],[214,212],[212,207],[207,214],[183,42],[42,184],[184,183],[210,169],[169,211],[211,210],[140,170],[170,176],[176,140],[104,105],[105,69],[69,104],[193,122],[122,168],
|
||||||
|
[168,193],[50,123],[123,187],[187,50],[89,96],[96,90],[90,89],[66,65],[65,107],[107,66],[179,89],[89,180],[180,179],[119,101],[101,120],[120,119],[68,63],[63,104],[104,68],[234,93],[93,227],[227,234],[16,15],[15,85],[85,16],[209,129],[129,49],[49,209],[15,14],[14,86],[86,15],[107,55],[55,9],[9,107],[120,100],[100,121],[121,120],[153,145],[145,22],[22,153],[178,88],[88,179],[179,178],[197,6],[6,196],[196,197],[89,88],[88,96],[96,89],[135,138],[138,136],[136,135],[138,215],[215,172],[172,138],[218,
|
||||||
|
115],[115,219],[219,218],[41,42],[42,81],[81,41],[5,195],[195,51],[51,5],[57,43],[43,61],[61,57],[208,171],[171,199],[199,208],[41,81],[81,38],[38,41],[224,53],[53,225],[225,224],[24,144],[144,110],[110,24],[105,52],[52,66],[66,105],[118,229],[229,117],[117,118],[227,34],[34,234],[234,227],[66,107],[107,69],[69,66],[10,109],[109,151],[151,10],[219,48],[48,235],[235,219],[183,62],[62,191],[191,183],[142,129],[129,126],[126,142],[116,111],[111,143],[143,116],[118,117],[117,50],[50,118],[223,222],[222,
|
||||||
|
52],[52,223],[94,19],[19,141],[141,94],[222,221],[221,65],[65,222],[196,3],[3,197],[197,196],[45,220],[220,44],[44,45],[156,70],[70,139],[139,156],[188,122],[122,245],[245,188],[139,71],[71,162],[162,139],[149,170],[170,150],[150,149],[122,188],[188,196],[196,122],[206,216],[216,92],[92,206],[164,2],[2,167],[167,164],[242,141],[141,241],[241,242],[0,164],[164,37],[37,0],[11,72],[72,12],[12,11],[12,38],[38,13],[13,12],[70,63],[63,71],[71,70],[31,226],[226,111],[111,31],[36,101],[101,205],[205,36],
|
||||||
|
[203,206],[206,165],[165,203],[126,209],[209,217],[217,126],[98,165],[165,97],[97,98],[237,220],[220,218],[218,237],[237,239],[239,241],[241,237],[210,214],[214,169],[169,210],[140,171],[171,32],[32,140],[241,125],[125,237],[237,241],[179,86],[86,178],[178,179],[180,85],[85,179],[179,180],[181,84],[84,180],[180,181],[182,83],[83,181],[181,182],[194,201],[201,182],[182,194],[177,137],[137,132],[132,177],[184,76],[76,183],[183,184],[185,61],[61,184],[184,185],[186,57],[57,185],[185,186],[216,212],[212,
|
||||||
|
186],[186,216],[192,214],[214,187],[187,192],[139,34],[34,156],[156,139],[218,79],[79,237],[237,218],[147,123],[123,177],[177,147],[45,44],[44,4],[4,45],[208,201],[201,32],[32,208],[98,64],[64,129],[129,98],[192,213],[213,138],[138,192],[235,59],[59,219],[219,235],[141,242],[242,97],[97,141],[97,2],[2,141],[141,97],[240,75],[75,235],[235,240],[229,24],[24,228],[228,229],[31,25],[25,226],[226,31],[230,23],[23,229],[229,230],[231,22],[22,230],[230,231],[232,26],[26,231],[231,232],[233,112],[112,232],
|
||||||
|
[232,233],[244,189],[189,243],[243,244],[189,221],[221,190],[190,189],[222,28],[28,221],[221,222],[223,27],[27,222],[222,223],[224,29],[29,223],[223,224],[225,30],[30,224],[224,225],[113,247],[247,225],[225,113],[99,60],[60,240],[240,99],[213,147],[147,215],[215,213],[60,20],[20,166],[166,60],[192,187],[187,213],[213,192],[243,112],[112,244],[244,243],[244,233],[233,245],[245,244],[245,128],[128,188],[188,245],[188,114],[114,174],[174,188],[134,131],[131,220],[220,134],[174,217],[217,236],[236,174],
|
||||||
|
[236,198],[198,134],[134,236],[215,177],[177,58],[58,215],[156,143],[143,124],[124,156],[25,110],[110,7],[7,25],[31,228],[228,25],[25,31],[264,356],[356,368],[368,264],[0,11],[11,267],[267,0],[451,452],[452,349],[349,451],[267,302],[302,269],[269,267],[350,357],[357,277],[277,350],[350,452],[452,357],[357,350],[299,333],[333,297],[297,299],[396,175],[175,377],[377,396],[280,347],[347,330],[330,280],[269,303],[303,270],[270,269],[151,9],[9,337],[337,151],[344,278],[278,360],[360,344],[424,418],[418,
|
||||||
|
431],[431,424],[270,304],[304,409],[409,270],[272,310],[310,407],[407,272],[322,270],[270,410],[410,322],[449,450],[450,347],[347,449],[432,422],[422,434],[434,432],[18,313],[313,17],[17,18],[291,306],[306,375],[375,291],[259,387],[387,260],[260,259],[424,335],[335,418],[418,424],[434,364],[364,416],[416,434],[391,423],[423,327],[327,391],[301,251],[251,298],[298,301],[275,281],[281,4],[4,275],[254,373],[373,253],[253,254],[375,307],[307,321],[321,375],[280,425],[425,411],[411,280],[200,421],[421,
|
||||||
|
18],[18,200],[335,321],[321,406],[406,335],[321,320],[320,405],[405,321],[314,315],[315,17],[17,314],[423,426],[426,266],[266,423],[396,377],[377,369],[369,396],[270,322],[322,269],[269,270],[413,417],[417,464],[464,413],[385,386],[386,258],[258,385],[248,456],[456,419],[419,248],[298,284],[284,333],[333,298],[168,417],[417,8],[8,168],[448,346],[346,261],[261,448],[417,413],[413,285],[285,417],[326,327],[327,328],[328,326],[277,355],[355,329],[329,277],[309,392],[392,438],[438,309],[381,382],[382,
|
||||||
|
256],[256,381],[279,429],[429,360],[360,279],[365,364],[364,379],[379,365],[355,277],[277,437],[437,355],[282,443],[443,283],[283,282],[281,275],[275,363],[363,281],[395,431],[431,369],[369,395],[299,297],[297,337],[337,299],[335,273],[273,321],[321,335],[348,450],[450,349],[349,348],[359,446],[446,467],[467,359],[283,293],[293,282],[282,283],[250,458],[458,462],[462,250],[300,276],[276,383],[383,300],[292,308],[308,325],[325,292],[283,276],[276,293],[293,283],[264,372],[372,447],[447,264],[346,352],
|
||||||
|
[352,340],[340,346],[354,274],[274,19],[19,354],[363,456],[456,281],[281,363],[426,436],[436,425],[425,426],[380,381],[381,252],[252,380],[267,269],[269,393],[393,267],[421,200],[200,428],[428,421],[371,266],[266,329],[329,371],[432,287],[287,422],[422,432],[290,250],[250,328],[328,290],[385,258],[258,384],[384,385],[446,265],[265,342],[342,446],[386,387],[387,257],[257,386],[422,424],[424,430],[430,422],[445,342],[342,276],[276,445],[422,273],[273,424],[424,422],[306,292],[292,307],[307,306],[352,
|
||||||
|
366],[366,345],[345,352],[268,271],[271,302],[302,268],[358,423],[423,371],[371,358],[327,294],[294,460],[460,327],[331,279],[279,294],[294,331],[303,271],[271,304],[304,303],[436,432],[432,427],[427,436],[304,272],[272,408],[408,304],[395,394],[394,431],[431,395],[378,395],[395,400],[400,378],[296,334],[334,299],[299,296],[6,351],[351,168],[168,6],[376,352],[352,411],[411,376],[307,325],[325,320],[320,307],[285,295],[295,336],[336,285],[320,319],[319,404],[404,320],[329,330],[330,349],[349,329],
|
||||||
|
[334,293],[293,333],[333,334],[366,323],[323,447],[447,366],[316,15],[15,315],[315,316],[331,358],[358,279],[279,331],[317,14],[14,316],[316,317],[8,285],[285,9],[9,8],[277,329],[329,350],[350,277],[253,374],[374,252],[252,253],[319,318],[318,403],[403,319],[351,6],[6,419],[419,351],[324,318],[318,325],[325,324],[397,367],[367,365],[365,397],[288,435],[435,397],[397,288],[278,344],[344,439],[439,278],[310,272],[272,311],[311,310],[248,195],[195,281],[281,248],[375,273],[273,291],[291,375],[175,396],
|
||||||
|
[396,199],[199,175],[312,311],[311,268],[268,312],[276,283],[283,445],[445,276],[390,373],[373,339],[339,390],[295,282],[282,296],[296,295],[448,449],[449,346],[346,448],[356,264],[264,454],[454,356],[337,336],[336,299],[299,337],[337,338],[338,151],[151,337],[294,278],[278,455],[455,294],[308,292],[292,415],[415,308],[429,358],[358,355],[355,429],[265,340],[340,372],[372,265],[352,346],[346,280],[280,352],[295,442],[442,282],[282,295],[354,19],[19,370],[370,354],[285,441],[441,295],[295,285],[195,
|
||||||
|
248],[248,197],[197,195],[457,440],[440,274],[274,457],[301,300],[300,368],[368,301],[417,351],[351,465],[465,417],[251,301],[301,389],[389,251],[394,395],[395,379],[379,394],[399,412],[412,419],[419,399],[410,436],[436,322],[322,410],[326,2],[2,393],[393,326],[354,370],[370,461],[461,354],[393,164],[164,267],[267,393],[268,302],[302,12],[12,268],[312,268],[268,13],[13,312],[298,293],[293,301],[301,298],[265,446],[446,340],[340,265],[280,330],[330,425],[425,280],[322,426],[426,391],[391,322],[420,
|
||||||
|
429],[429,437],[437,420],[393,391],[391,326],[326,393],[344,440],[440,438],[438,344],[458,459],[459,461],[461,458],[364,434],[434,394],[394,364],[428,396],[396,262],[262,428],[274,354],[354,457],[457,274],[317,316],[316,402],[402,317],[316,315],[315,403],[403,316],[315,314],[314,404],[404,315],[314,313],[313,405],[405,314],[313,421],[421,406],[406,313],[323,366],[366,361],[361,323],[292,306],[306,407],[407,292],[306,291],[291,408],[408,306],[291,287],[287,409],[409,291],[287,432],[432,410],[410,287],
|
||||||
|
[427,434],[434,411],[411,427],[372,264],[264,383],[383,372],[459,309],[309,457],[457,459],[366,352],[352,401],[401,366],[1,274],[274,4],[4,1],[418,421],[421,262],[262,418],[331,294],[294,358],[358,331],[435,433],[433,367],[367,435],[392,289],[289,439],[439,392],[328,462],[462,326],[326,328],[94,2],[2,370],[370,94],[289,305],[305,455],[455,289],[339,254],[254,448],[448,339],[359,255],[255,446],[446,359],[254,253],[253,449],[449,254],[253,252],[252,450],[450,253],[252,256],[256,451],[451,252],[256,
|
||||||
|
341],[341,452],[452,256],[414,413],[413,463],[463,414],[286,441],[441,414],[414,286],[286,258],[258,441],[441,286],[258,257],[257,442],[442,258],[257,259],[259,443],[443,257],[259,260],[260,444],[444,259],[260,467],[467,445],[445,260],[309,459],[459,250],[250,309],[305,289],[289,290],[290,305],[305,290],[290,460],[460,305],[401,376],[376,435],[435,401],[309,250],[250,392],[392,309],[376,411],[411,433],[433,376],[453,341],[341,464],[464,453],[357,453],[453,465],[465,357],[343,357],[357,412],[412,343],
|
||||||
|
[437,343],[343,399],[399,437],[344,360],[360,440],[440,344],[420,437],[437,456],[456,420],[360,420],[420,363],[363,360],[361,401],[401,288],[288,361],[265,372],[372,353],[353,265],[390,339],[339,249],[249,390],[339,448],[448,255],[255,339]]);P("matrixDataToMatrix",function(a){for(var b=a.getCols(),c=a.getRows(),d=a.getPackedDataList(),e=[],g=0;g<c;g++)e.push(Array(b));for(g=0;g<c;g++)for(var f=0;f<b;f++){var h=1===a.getLayout()?g*b+f:f*c+g;e[g][f]=d[h]}return e});P("VERSION","0.4.1633559619");}).call(this);
|
||||||
Binary file not shown.
@@ -0,0 +1,199 @@
|
|||||||
|
|
||||||
|
var Module = typeof createMediapipeSolutionsPackedAssets !== 'undefined' ? createMediapipeSolutionsPackedAssets : {};
|
||||||
|
|
||||||
|
if (!Module.expectedDataFileDownloads) {
|
||||||
|
Module.expectedDataFileDownloads = 0;
|
||||||
|
}
|
||||||
|
Module.expectedDataFileDownloads++;
|
||||||
|
(function() {
|
||||||
|
var loadPackage = function(metadata) {
|
||||||
|
|
||||||
|
var PACKAGE_PATH = '';
|
||||||
|
if (typeof window === 'object') {
|
||||||
|
PACKAGE_PATH = window['encodeURIComponent'](window.location.pathname.toString().substring(0, window.location.pathname.toString().lastIndexOf('/')) + '/');
|
||||||
|
} else if (typeof process === 'undefined' && typeof location !== 'undefined') {
|
||||||
|
// web worker
|
||||||
|
PACKAGE_PATH = encodeURIComponent(location.pathname.toString().substring(0, location.pathname.toString().lastIndexOf('/')) + '/');
|
||||||
|
}
|
||||||
|
var PACKAGE_NAME = 'blaze-out/k8-opt/genfiles/third_party/mediapipe/web/solutions/face_mesh/face_mesh_solution_packed_assets.data';
|
||||||
|
var REMOTE_PACKAGE_BASE = 'face_mesh_solution_packed_assets.data';
|
||||||
|
if (typeof Module['locateFilePackage'] === 'function' && !Module['locateFile']) {
|
||||||
|
Module['locateFile'] = Module['locateFilePackage'];
|
||||||
|
err('warning: you defined Module.locateFilePackage, that has been renamed to Module.locateFile (using your locateFilePackage for now)');
|
||||||
|
}
|
||||||
|
var REMOTE_PACKAGE_NAME = Module['locateFile'] ? Module['locateFile'](REMOTE_PACKAGE_BASE, '') : REMOTE_PACKAGE_BASE;
|
||||||
|
|
||||||
|
var REMOTE_PACKAGE_SIZE = metadata['remote_package_size'];
|
||||||
|
var PACKAGE_UUID = metadata['package_uuid'];
|
||||||
|
|
||||||
|
function fetchRemotePackage(packageName, packageSize, callback, errback) {
|
||||||
|
|
||||||
|
if (typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string') {
|
||||||
|
require('fs').readFile(packageName, function(err, contents) {
|
||||||
|
if (err) {
|
||||||
|
errback(err);
|
||||||
|
} else {
|
||||||
|
callback(contents.buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', packageName, true);
|
||||||
|
xhr.responseType = 'arraybuffer';
|
||||||
|
xhr.onprogress = function(event) {
|
||||||
|
var url = packageName;
|
||||||
|
var size = packageSize;
|
||||||
|
if (event.total) size = event.total;
|
||||||
|
if (event.loaded) {
|
||||||
|
if (!xhr.addedTotal) {
|
||||||
|
xhr.addedTotal = true;
|
||||||
|
if (!Module.dataFileDownloads) Module.dataFileDownloads = {};
|
||||||
|
Module.dataFileDownloads[url] = {
|
||||||
|
loaded: event.loaded,
|
||||||
|
total: size
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
Module.dataFileDownloads[url].loaded = event.loaded;
|
||||||
|
}
|
||||||
|
var total = 0;
|
||||||
|
var loaded = 0;
|
||||||
|
var num = 0;
|
||||||
|
for (var download in Module.dataFileDownloads) {
|
||||||
|
var data = Module.dataFileDownloads[download];
|
||||||
|
total += data.total;
|
||||||
|
loaded += data.loaded;
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
total = Math.ceil(total * Module.expectedDataFileDownloads/num);
|
||||||
|
if (Module['setStatus']) Module['setStatus']('Downloading data... (' + loaded + '/' + total + ')');
|
||||||
|
} else if (!Module.dataFileDownloads) {
|
||||||
|
if (Module['setStatus']) Module['setStatus']('Downloading data...');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function(event) {
|
||||||
|
throw new Error("NetworkError for: " + packageName);
|
||||||
|
}
|
||||||
|
xhr.onload = function(event) {
|
||||||
|
if (xhr.status == 200 || xhr.status == 304 || xhr.status == 206 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0
|
||||||
|
var packageData = xhr.response;
|
||||||
|
callback(packageData);
|
||||||
|
} else {
|
||||||
|
throw new Error(xhr.statusText + " : " + xhr.responseURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleError(error) {
|
||||||
|
console.error('package error:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
var fetchedCallback = null;
|
||||||
|
var fetched = Module['getPreloadedPackage'] ? Module['getPreloadedPackage'](REMOTE_PACKAGE_NAME, REMOTE_PACKAGE_SIZE) : null;
|
||||||
|
|
||||||
|
if (!fetched) fetchRemotePackage(REMOTE_PACKAGE_NAME, REMOTE_PACKAGE_SIZE, function(data) {
|
||||||
|
if (fetchedCallback) {
|
||||||
|
fetchedCallback(data);
|
||||||
|
fetchedCallback = null;
|
||||||
|
} else {
|
||||||
|
fetched = data;
|
||||||
|
}
|
||||||
|
}, handleError);
|
||||||
|
|
||||||
|
function runWithFS() {
|
||||||
|
|
||||||
|
function assert(check, msg) {
|
||||||
|
if (!check) throw msg + new Error().stack;
|
||||||
|
}
|
||||||
|
Module['FS_createPath']("/", "third_party", true, true);
|
||||||
|
Module['FS_createPath']("/third_party", "mediapipe", true, true);
|
||||||
|
Module['FS_createPath']("/third_party/mediapipe", "modules", true, true);
|
||||||
|
Module['FS_createPath']("/third_party/mediapipe/modules", "face_landmark", true, true);
|
||||||
|
Module['FS_createPath']("/third_party/mediapipe/modules", "face_geometry", true, true);
|
||||||
|
Module['FS_createPath']("/third_party/mediapipe/modules/face_geometry", "data", true, true);
|
||||||
|
Module['FS_createPath']("/third_party/mediapipe/modules", "face_detection", true, true);
|
||||||
|
|
||||||
|
/** @constructor */
|
||||||
|
function DataRequest(start, end, audio) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.audio = audio;
|
||||||
|
}
|
||||||
|
DataRequest.prototype = {
|
||||||
|
requests: {},
|
||||||
|
open: function(mode, name) {
|
||||||
|
this.name = name;
|
||||||
|
this.requests[name] = this;
|
||||||
|
Module['addRunDependency']('fp ' + this.name);
|
||||||
|
},
|
||||||
|
send: function() {},
|
||||||
|
onload: function() {
|
||||||
|
var byteArray = this.byteArray.subarray(this.start, this.end);
|
||||||
|
this.finish(byteArray);
|
||||||
|
},
|
||||||
|
finish: function(byteArray) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
Module['FS_createPreloadedFile'](this.name, null, byteArray, true, true, function() {
|
||||||
|
Module['removeRunDependency']('fp ' + that.name);
|
||||||
|
}, function() {
|
||||||
|
if (that.audio) {
|
||||||
|
Module['removeRunDependency']('fp ' + that.name); // workaround for chromium bug 124926 (still no audio with this, but at least we don't hang)
|
||||||
|
} else {
|
||||||
|
err('Preloading file ' + that.name + ' failed');
|
||||||
|
}
|
||||||
|
}, false, true); // canOwn this data in the filesystem, it is a slide into the heap that will never change
|
||||||
|
|
||||||
|
this.requests[this.name] = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var files = metadata['files'];
|
||||||
|
for (var i = 0; i < files.length; ++i) {
|
||||||
|
new DataRequest(files[i]['start'], files[i]['end'], files[i]['audio']).open('GET', files[i]['filename']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function processPackageData(arrayBuffer) {
|
||||||
|
assert(arrayBuffer, 'Loading data file failed.');
|
||||||
|
assert(arrayBuffer instanceof ArrayBuffer, 'bad input to processPackageData');
|
||||||
|
var byteArray = new Uint8Array(arrayBuffer);
|
||||||
|
var curr;
|
||||||
|
|
||||||
|
// Reuse the bytearray from the XHR as the source for file reads.
|
||||||
|
DataRequest.prototype.byteArray = byteArray;
|
||||||
|
|
||||||
|
var files = metadata['files'];
|
||||||
|
for (var i = 0; i < files.length; ++i) {
|
||||||
|
DataRequest.prototype.requests[files[i].filename].onload();
|
||||||
|
}
|
||||||
|
Module['removeRunDependency']('datafile_blaze-out/k8-opt/genfiles/third_party/mediapipe/web/solutions/face_mesh/face_mesh_solution_packed_assets.data');
|
||||||
|
|
||||||
|
};
|
||||||
|
Module['addRunDependency']('datafile_blaze-out/k8-opt/genfiles/third_party/mediapipe/web/solutions/face_mesh/face_mesh_solution_packed_assets.data');
|
||||||
|
|
||||||
|
if (!Module.preloadResults) Module.preloadResults = {};
|
||||||
|
|
||||||
|
Module.preloadResults[PACKAGE_NAME] = {fromCache: false};
|
||||||
|
if (fetched) {
|
||||||
|
processPackageData(fetched);
|
||||||
|
fetched = null;
|
||||||
|
} else {
|
||||||
|
fetchedCallback = processPackageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (Module['calledRun']) {
|
||||||
|
runWithFS();
|
||||||
|
} else {
|
||||||
|
if (!Module['preRun']) Module['preRun'] = [];
|
||||||
|
Module["preRun"].push(runWithFS); // FS is not initialized yet, wait for it
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
loadPackage({"files": [{"filename": "/third_party/mediapipe/modules/face_landmark/face_landmark_with_attention.tflite", "start": 0, "end": 2495952, "audio": 0}, {"filename": "/third_party/mediapipe/modules/face_landmark/face_landmark.tflite", "start": 2495952, "end": 3737848, "audio": 0}, {"filename": "/third_party/mediapipe/modules/face_geometry/data/geometry_pipeline_metadata_landmarks.binarypb", "start": 3737848, "end": 3757224, "audio": 0}, {"filename": "/third_party/mediapipe/modules/face_detection/face_detection_short_range.tflite", "start": 3757224, "end": 3986256, "audio": 0}], "remote_package_size": 3986256, "package_uuid": "f5f855ab-ba1b-4fdf-8b0a-77c2c611502f"});
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
+8
-3
@@ -1,8 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="it" class="notranslate">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="google" content="notranslate"/>
|
||||||
<title>CantiCristiani</title>
|
<title>CantiCristiani</title>
|
||||||
|
|
||||||
<base href="/"/>
|
<base href="/"/>
|
||||||
@@ -73,7 +74,11 @@
|
|||||||
if (options.percent !== undefined) {
|
if (options.percent !== undefined) {
|
||||||
const pct = Math.min(100, Math.max(0, Math.round(options.percent)));
|
const pct = Math.min(100, Math.max(0, Math.round(options.percent)));
|
||||||
if (bar) bar.style.width = pct + '%';
|
if (bar) bar.style.width = pct + '%';
|
||||||
if (pctText) pctText.textContent = pct + '%';
|
if (bar && bar.parentElement) bar.parentElement.style.display = 'block';
|
||||||
|
if (pctText) {
|
||||||
|
pctText.style.display = 'block';
|
||||||
|
pctText.textContent = pct + '%';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.isRedirect) {
|
if (options.isRedirect) {
|
||||||
@@ -82,7 +87,7 @@
|
|||||||
if (phaseEl) phaseEl.style.display = 'none';
|
if (phaseEl) phaseEl.style.display = 'none';
|
||||||
if (versionEl) versionEl.style.display = 'none';
|
if (versionEl) versionEl.style.display = 'none';
|
||||||
if (titleEl) {
|
if (titleEl) {
|
||||||
titleEl.textContent = 'Chiudi il browser, è stata aperta la app installata sul device';
|
titleEl.textContent = 'Chiudi il browser è stata installata la app sul device';
|
||||||
titleEl.style.fontSize = '1.4rem';
|
titleEl.style.fontSize = '1.4rem';
|
||||||
titleEl.style.lineHeight = '1.5';
|
titleEl.style.lineHeight = '1.5';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user