Refactor: Restringimento drag & drop, legenda impostazioni, modalità offline e rimozione sezione liturgia
This commit is contained in:
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# --- Caricamento variabili d'ambiente ---
|
||||||
|
if [ -f .env ]; then
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
else
|
||||||
|
echo "❌ Errore: File .env non trovato."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Configurazione FTP ---
|
||||||
|
FTP_HOST="ftp.canticristiani.it"
|
||||||
|
FTP_USER="canticristiani.it"
|
||||||
|
FTP_PASS="$FTP_PASSWORD"
|
||||||
|
SOURCE_URL="http://185.193.67.105:3000/cantiletture.json"
|
||||||
|
REMOTE_PATH="api/cantiletture.json"
|
||||||
|
|
||||||
|
if [ -z "$FTP_PASS" ]; then
|
||||||
|
echo "❌ Errore: FTP_PASSWORD non definita nel file .env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Download del file ---
|
||||||
|
echo "⬇️ Scaricamento dati da $SOURCE_URL..."
|
||||||
|
TEMP_FILE=$(mktemp)
|
||||||
|
if ! curl -s -L "$SOURCE_URL" -o "$TEMP_FILE"; then
|
||||||
|
echo "❌ Errore nel download dei dati."
|
||||||
|
rm "$TEMP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Upload via FTP ---
|
||||||
|
echo "🚀 Caricamento nuovo file su $FTP_HOST/$REMOTE_PATH..."
|
||||||
|
if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then
|
||||||
|
echo "✅ Allineamento completato con successo!"
|
||||||
|
else
|
||||||
|
echo "❌ Errore durante il caricamento FTP."
|
||||||
|
rm "$TEMP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm "$TEMP_FILE"
|
||||||
@@ -27,10 +27,6 @@ const routes: Routes = [
|
|||||||
path: 'propose-canto',
|
path: 'propose-canto',
|
||||||
loadComponent: () => import('./pages/propose-canto/propose-canto.page').then( m => m.ProposeCantoPage)
|
loadComponent: () => import('./pages/propose-canto/propose-canto.page').then( m => m.ProposeCantoPage)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'liturgia',
|
|
||||||
loadChildren: () => import('./pages/liturgia/liturgia.module').then( m => m.LiturgiaPageModule)
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
+74
-11
@@ -10,10 +10,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</ion-title>
|
</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
|
||||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<ion-button (click)="importPlaylist()" class="add-btn">
|
<ion-button (click)="importPlaylist()" class="add-btn">
|
||||||
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
|
||||||
@@ -83,6 +79,17 @@
|
|||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
</span>
|
</span>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
|
<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
|
<ion-button
|
||||||
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
|
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
|
||||||
@@ -96,6 +103,17 @@
|
|||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
</span>
|
</span>
|
||||||
</ion-button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +126,9 @@
|
|||||||
<!-- Selection Mode Actions -->
|
<!-- Selection Mode Actions -->
|
||||||
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
|
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
|
||||||
<span class="selection-count">{{ playlistService.selectedIds().size }}</span>
|
<span class="selection-count">{{ playlistService.selectedIds().size }}</span>
|
||||||
|
<ion-button fill="clear" color="secondary" (click)="toggleAddingSongs()" class="mini-action-btn">
|
||||||
|
<ion-icon slot="icon-only" [name]="isAddingSongs() ? 'list-outline' : 'add-circle-outline'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
<ion-button fill="clear" color="secondary" (click)="finishSelection()" [disabled]="playlistService.selectedIds().size === 0" class="mini-action-btn">
|
<ion-button fill="clear" color="secondary" (click)="finishSelection()" [disabled]="playlistService.selectedIds().size === 0" class="mini-action-btn">
|
||||||
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -162,6 +183,32 @@
|
|||||||
(cdkDropListDropped)="drop($event)"
|
(cdkDropListDropped)="drop($event)"
|
||||||
class="transparent-list">
|
class="transparent-list">
|
||||||
|
|
||||||
|
<!-- Premium Suggested Mass Card (Collapsible) -->
|
||||||
|
<div class="suggeriti-header-card glass ion-margin-bottom"
|
||||||
|
*ngIf="showSuggeriti() && cantiLettureService.data()"
|
||||||
|
(click)="isMassCardExpanded.set(!isMassCardExpanded())"
|
||||||
|
[class.expanded]="isMassCardExpanded()">
|
||||||
|
<div class="card-inner ion-padding">
|
||||||
|
<div class="card-header-row">
|
||||||
|
<div class="card-header-left">
|
||||||
|
<span class="card-label">SINTESI MESSA</span>
|
||||||
|
<span class="card-date-compact" *ngIf="!isMassCardExpanded()">{{ getSelectedMassFormattedDate() }}</span>
|
||||||
|
</div>
|
||||||
|
<ion-icon [name]="isMassCardExpanded() ? 'chevron-up-outline' : 'chevron-down-outline'" class="expand-icon"></ion-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="expandable-content" [class.show]="isMassCardExpanded()">
|
||||||
|
<div class="card-date-row" *ngIf="isMassCardExpanded()">
|
||||||
|
<span class="card-date">{{ getSelectedMassFormattedDate() }}</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="card-title outfit-font">{{ getSelectedMassTitle() }}</h1>
|
||||||
|
<p class="card-summary outfit-font" *ngIf="getSelectedMassSummary()">
|
||||||
|
{{ getSelectedMassSummary() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-item *ngFor="let canto of visibleCanti()"
|
<ion-item *ngFor="let canto of visibleCanti()"
|
||||||
[id]="'canto-' + canto.id"
|
[id]="'canto-' + canto.id"
|
||||||
class="glass ion-margin-bottom custom-item"
|
class="glass ion-margin-bottom custom-item"
|
||||||
@@ -189,13 +236,23 @@
|
|||||||
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary)">
|
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary)">
|
||||||
{{ canto.titolo }}
|
{{ canto.titolo }}
|
||||||
</h2>
|
</h2>
|
||||||
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px;">{{ canto.autore || 'Autore sconosciuto' }}</p>
|
<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 *ngIf="showTopTen() && getEsecuzioniCount(canto.id) !== null" class="esecuzioni-badge" style="color: var(--ion-color-secondary); font-size: 0.75rem; background: rgba(var(--ion-color-secondary-rgb), 0.12); padding: 2px 6px; border-radius: 6px; display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.2);">
|
||||||
|
<ion-icon name="stats-chart-outline" style="font-size: 0.75rem; color: var(--ion-color-secondary);"></ion-icon>
|
||||||
|
{{ getEsecuzioniCount(canto.id) }} esecuzioni
|
||||||
|
</span>
|
||||||
|
<span *ngIf="showSuggeriti() && getMassSuggestionWeight(canto.id_canti) !== null" class="suggerito-badge" style="color: var(--ion-color-secondary); font-size: 0.75rem; background: rgba(var(--ion-color-secondary-rgb), 0.12); padding: 2px 6px; border-radius: 6px; display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);">
|
||||||
|
<ion-icon name="sparkles-outline" style="font-size: 0.75rem; color: var(--ion-color-secondary);"></ion-icon>
|
||||||
|
Attinenza {{ getMassSuggestionWeight(canto.id_canti) }}%
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|
||||||
<!-- Video Thumbnail Section -->
|
<!-- Video Thumbnail Section -->
|
||||||
<div *ngIf="canto.link_youtube && canto.link_youtube.length > 5" class="thumb-section" (click)="$event.stopPropagation()">
|
<div *ngIf="canto.link_youtube && canto.link_youtube.length > 5" class="thumb-section" (click)="$event.stopPropagation()">
|
||||||
<div class="thumb-container compact">
|
<div class="thumb-container compact">
|
||||||
<ng-container *ngIf="connectivityService.isOnline(); else offlineThumb">
|
<ng-container *ngIf="youtubePlayerService.isPlayerSupported(); else noPlayerThumb">
|
||||||
<div class="thumb-wrapper" (click)="playVideo($event, canto.id)">
|
<div class="thumb-wrapper" (click)="playVideo($event, canto.id)">
|
||||||
<img [src]="cantiService.getYoutubeThumb(canto.link_youtube)"
|
<img [src]="cantiService.getYoutubeThumb(canto.link_youtube)"
|
||||||
class="thumb-img loaded"
|
class="thumb-img loaded"
|
||||||
@@ -205,11 +262,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #offlineThumb>
|
<ng-template #noPlayerThumb>
|
||||||
<div class="thumb-wrapper">
|
<div class="thumb-wrapper">
|
||||||
<div class="offline-thumb">
|
<img [src]="cantiService.getYoutubeThumb(canto.link_youtube)"
|
||||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
class="thumb-img loaded"
|
||||||
</div>
|
loading="lazy"
|
||||||
|
*ngIf="connectivityService.isOnline(); else offlineIcon">
|
||||||
|
<ng-template #offlineIcon>
|
||||||
|
<div class="offline-thumb">
|
||||||
|
<ion-icon name="cloud-offline-outline"></ion-icon>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +312,7 @@
|
|||||||
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
<ion-footer *ngIf="youtubePlayerService.currentCantoId()" class="ion-no-border">
|
<ion-footer *ngIf="youtubePlayerService.isPlayerSupported() && youtubePlayerService.currentCantoId()" class="ion-no-border">
|
||||||
<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">
|
||||||
|
|||||||
+143
-3
@@ -31,7 +31,7 @@
|
|||||||
.search-wrapper-group {
|
.search-wrapper-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 8px 16px 12px 16px; // Added top padding for extra clearance
|
padding: 2px 16px 12px 16px; // Reduced top padding
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ ion-title {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 20px 0 16px 24px; // Increased padding for mobile spacing
|
padding: 16px 0 4px 24px; // Reduced padding to bring search bar closer
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
@@ -661,7 +661,7 @@ ion-title {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 12px 0 0;
|
padding: 0 12px 0 0;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
color: rgba(255, 255, 255, 0.3);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
@@ -748,3 +748,143 @@ ion-title {
|
|||||||
.transparent-list {
|
.transparent-list {
|
||||||
padding-bottom: 100px !important;
|
padding-bottom: 100px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.suggeriti-header-card {
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease, border-color 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: padding-bottom 0.3s ease, border-bottom-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
.card-header-row {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date-compact {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-content {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease, margin-top 0.35s ease;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
max-height: 400px; /* High enough to fit summary */
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date-row {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-summary {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body.high-contrast) .suggeriti-header-card {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-color: #000000 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
.card-date-compact {
|
||||||
|
color: black !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
.card-header-row {
|
||||||
|
border-bottom-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-summary {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body.high-contrast) {
|
||||||
|
.drag-handle {
|
||||||
|
color: rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+136
-12
@@ -13,6 +13,7 @@ import { ModalController, AlertController, ToastController } from '@ionic/angula
|
|||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { MyCantiService } from '../services/my-canti.service';
|
import { MyCantiService } from '../services/my-canti.service';
|
||||||
import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component';
|
import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component';
|
||||||
|
import { CantiLettureService } from '../services/canti-letture.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -25,6 +26,9 @@ export class HomePage implements OnDestroy {
|
|||||||
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);
|
||||||
|
public showTopTen = signal<boolean>(false);
|
||||||
|
public showSuggeriti = signal<boolean>(false);
|
||||||
|
public isMassCardExpanded = signal<boolean>(false);
|
||||||
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null);
|
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null);
|
||||||
public loadedThumbs = new Set<string>();
|
public loadedThumbs = new Set<string>();
|
||||||
public version = VERSION;
|
public version = VERSION;
|
||||||
@@ -49,6 +53,7 @@ export class HomePage implements OnDestroy {
|
|||||||
public myCantiService = inject(MyCantiService);
|
public myCantiService = inject(MyCantiService);
|
||||||
public playlistService = inject(PlaylistService);
|
public playlistService = inject(PlaylistService);
|
||||||
public settingsService = inject(SettingsService);
|
public settingsService = inject(SettingsService);
|
||||||
|
public cantiLettureService = inject(CantiLettureService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
private modalCtrl = inject(ModalController);
|
private modalCtrl = inject(ModalController);
|
||||||
@@ -93,6 +98,8 @@ export class HomePage implements OnDestroy {
|
|||||||
const litId = this.selectedLiturgico();
|
const litId = this.selectedLiturgico();
|
||||||
const temId = this.selectedTematico();
|
const temId = this.selectedTematico();
|
||||||
const onlyMine = this.showOnlyMine();
|
const onlyMine = this.showOnlyMine();
|
||||||
|
const topTen = this.showTopTen();
|
||||||
|
const suggeriti = this.showSuggeriti();
|
||||||
|
|
||||||
let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
||||||
|
|
||||||
@@ -107,12 +114,17 @@ export class HomePage implements OnDestroy {
|
|||||||
list = list.filter(c => c.id_momenti?.includes(temId));
|
list = list.filter(c => c.id_momenti?.includes(temId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (suggeriti) {
|
||||||
|
const suggMap = this.cantiLettureService.suggestionsMap();
|
||||||
|
list = list.filter(c => suggMap.has(c.id_canti));
|
||||||
|
}
|
||||||
|
|
||||||
// Special List handling (from QR Code or Saved Playlists)
|
// Special List handling (from QR Code or Saved Playlists)
|
||||||
const activeIds = this.playlistService.activeListIds();
|
const activeIds = this.playlistService.activeListIds();
|
||||||
const selectionMode = this.playlistService.selectionMode();
|
const selectionMode = this.playlistService.selectionMode();
|
||||||
|
|
||||||
// If we are in selection mode, only show restricted list if NOT adding songs AND we have a base list to reorder
|
// If we are in selection mode, only show restricted list if NOT adding songs
|
||||||
if (selectionMode && !this.isAddingSongs() && activeIds.length > 0) {
|
if (selectionMode && !this.isAddingSongs()) {
|
||||||
return this.reorderList();
|
return this.reorderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +136,25 @@ export class HomePage implements OnDestroy {
|
|||||||
.filter((c): c is any => !!c);
|
.filter((c): c is any => !!c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (suggeriti) {
|
||||||
|
const suggMap = this.cantiLettureService.suggestionsMap();
|
||||||
|
list = list.sort((a, b) => {
|
||||||
|
const pesoA = suggMap.get(a.id_canti) || 0;
|
||||||
|
const pesoB = suggMap.get(b.id_canti) || 0;
|
||||||
|
return pesoB - pesoA;
|
||||||
|
});
|
||||||
|
} else if (topTen) {
|
||||||
|
const eseguiti = this.cantiService.cantiEseguiti();
|
||||||
|
const eseguitiMap = new Map<number, number>();
|
||||||
|
eseguiti.forEach(x => eseguitiMap.set(x.id_canti, x.num));
|
||||||
|
|
||||||
|
list = list.sort((a, b) => {
|
||||||
|
const numA = eseguitiMap.get(a.id_canti) || 0;
|
||||||
|
const numB = eseguitiMap.get(b.id_canti) || 0;
|
||||||
|
return numB - numA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!query) return list;
|
if (!query) return list;
|
||||||
|
|
||||||
// Converte "uno" in "1" per facilitare la ricerca vocale (es. "numero uno")
|
// Converte "uno" in "1" per facilitare la ricerca vocale (es. "numero uno")
|
||||||
@@ -170,15 +201,22 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let prevSelectionMode = false;
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const selectionMode = this.playlistService.selectionMode();
|
const selectionMode = this.playlistService.selectionMode();
|
||||||
const selectedIds = this.playlistService.selectedIds();
|
const selectedIds = this.playlistService.selectedIds();
|
||||||
const activeIds = this.playlistService.activeListIds();
|
const activeIds = this.playlistService.activeListIds();
|
||||||
|
|
||||||
if (selectionMode) {
|
if (selectionMode) {
|
||||||
if (activeIds.length === 0 && !this.isAddingSongs()) {
|
if (!prevSelectionMode) {
|
||||||
this.isAddingSongs.set(true);
|
if (activeIds.length === 0) {
|
||||||
|
this.isAddingSongs.set(true);
|
||||||
|
} else {
|
||||||
|
this.isAddingSongs.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
prevSelectionMode = true;
|
||||||
|
|
||||||
const currentIds = this.reorderList().map(c => c.id);
|
const currentIds = this.reorderList().map(c => c.id);
|
||||||
const newIds = Array.from(selectedIds);
|
const newIds = Array.from(selectedIds);
|
||||||
@@ -208,6 +246,7 @@ export class HomePage implements OnDestroy {
|
|||||||
this.reorderList.set(updatedList);
|
this.reorderList.set(updatedList);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
prevSelectionMode = false;
|
||||||
this.isAddingSongs.set(false);
|
this.isAddingSongs.set(false);
|
||||||
}
|
}
|
||||||
}, { allowSignalWrites: true });
|
}, { allowSignalWrites: true });
|
||||||
@@ -267,11 +306,6 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToCanto(id: string) {
|
goToCanto(id: string) {
|
||||||
if (this.playlistService.selectionMode()) {
|
|
||||||
this.playlistService.toggleSongSelection(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: any = { id };
|
const params: any = { id };
|
||||||
const isPlaying = this.youtubePlayerService.isPlaying();
|
const isPlaying = this.youtubePlayerService.isPlaying();
|
||||||
|
|
||||||
@@ -379,6 +413,8 @@ export class HomePage implements OnDestroy {
|
|||||||
return this.selectedLiturgico() !== null ||
|
return this.selectedLiturgico() !== null ||
|
||||||
this.selectedTematico() !== null ||
|
this.selectedTematico() !== null ||
|
||||||
this.showOnlyMine() ||
|
this.showOnlyMine() ||
|
||||||
|
this.showTopTen() ||
|
||||||
|
this.showSuggeriti() ||
|
||||||
this.playlistService.activeListName() !== null ||
|
this.playlistService.activeListName() !== null ||
|
||||||
this.searchQuery() !== '';
|
this.searchQuery() !== '';
|
||||||
}
|
}
|
||||||
@@ -387,6 +423,8 @@ export class HomePage implements OnDestroy {
|
|||||||
this.selectedLiturgico.set(null);
|
this.selectedLiturgico.set(null);
|
||||||
this.selectedTematico.set(null);
|
this.selectedTematico.set(null);
|
||||||
this.showOnlyMine.set(false);
|
this.showOnlyMine.set(false);
|
||||||
|
this.showTopTen.set(false);
|
||||||
|
this.showSuggeriti.set(false);
|
||||||
this.playlistService.activeListIds.set([]);
|
this.playlistService.activeListIds.set([]);
|
||||||
this.playlistService.activeListName.set(null);
|
this.playlistService.activeListName.set(null);
|
||||||
this.playlistService.activePlaylistId.set(null);
|
this.playlistService.activePlaylistId.set(null);
|
||||||
@@ -415,6 +453,73 @@ export class HomePage implements OnDestroy {
|
|||||||
this.showOnlyMine.set(false);
|
this.showOnlyMine.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleTopTen() {
|
||||||
|
this.showTopTen.update(v => !v);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTopTen(event?: Event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
this.showTopTen.set(false);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSuggeriti() {
|
||||||
|
this.showSuggeriti.update(v => !v);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSuggeriti(event?: Event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
this.showSuggeriti.set(false);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMassSuggestionWeight(id_canti: number): number | null {
|
||||||
|
const weight = this.cantiLettureService.suggestionsMap().get(id_canti);
|
||||||
|
return weight !== undefined ? weight : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMassSuggestionMoment(id_canti: number): string | null {
|
||||||
|
const moments = this.cantiLettureService.suggestionsMomentsMap().get(id_canti);
|
||||||
|
return moments && moments.length > 0 ? moments.join(', ') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedMassFormattedDate(): string | null {
|
||||||
|
const dateStr = this.cantiLettureService.selectedMassDate();
|
||||||
|
if (!dateStr) return null;
|
||||||
|
const parts = dateStr.split('-');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||||
|
}
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedMassTitle(): string | null {
|
||||||
|
const dateStr = this.cantiLettureService.selectedMassDate();
|
||||||
|
const data = this.cantiLettureService.data();
|
||||||
|
if (dateStr && data && data.masses[dateStr]) {
|
||||||
|
return data.masses[dateStr].title;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedMassSummary(): string | null {
|
||||||
|
const dateStr = this.cantiLettureService.selectedMassDate();
|
||||||
|
const data = this.cantiLettureService.data();
|
||||||
|
if (dateStr && data && data.masses[dateStr]) {
|
||||||
|
return data.masses[dateStr].summary || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEsecuzioniCount(id: string): number | null {
|
||||||
|
const numId = parseInt(id, 10);
|
||||||
|
if (isNaN(numId)) return null;
|
||||||
|
const es = this.cantiService.cantiEseguiti().find(x => x.id_canti === numId);
|
||||||
|
return es ? es.num : null;
|
||||||
|
}
|
||||||
|
|
||||||
onImgLoad(id: string) {
|
onImgLoad(id: string) {
|
||||||
this.loadedThumbs.add(id);
|
this.loadedThumbs.add(id);
|
||||||
}
|
}
|
||||||
@@ -429,6 +534,9 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
playVideo(event: Event, id: string) {
|
playVideo(event: Event, id: string) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (!this.youtubePlayerService.isPlayerSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.youtubePlayerService.setMediaSessionCallbacks(
|
this.youtubePlayerService.setMediaSessionCallbacks(
|
||||||
() => this.playNextPreview(),
|
() => this.playNextPreview(),
|
||||||
() => this.playPrevPreview()
|
() => this.playPrevPreview()
|
||||||
@@ -577,14 +685,30 @@ export class HomePage implements OnDestroy {
|
|||||||
handleScannedData(data: string) {
|
handleScannedData(data: string) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
if (data.includes('?import=')) {
|
if (data.includes('import=')) {
|
||||||
try {
|
try {
|
||||||
const base64 = data.split('?import=')[1];
|
let base64 = '';
|
||||||
|
if (data.includes('?import=')) {
|
||||||
|
base64 = data.split('?import=')[1];
|
||||||
|
} else if (data.includes('&import=')) {
|
||||||
|
base64 = data.split('&import=')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base64.includes('&')) {
|
||||||
|
base64 = base64.split('&')[0];
|
||||||
|
}
|
||||||
|
if (base64.includes('#')) {
|
||||||
|
base64 = base64.split('#')[0];
|
||||||
|
}
|
||||||
|
|
||||||
this.handleImport(base64);
|
this.handleImport(base64);
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse scanned link', e);
|
console.error('Failed to parse scanned link', e);
|
||||||
}
|
}
|
||||||
} else if (data.startsWith('canti:')) {
|
}
|
||||||
|
|
||||||
|
if (data.startsWith('canti:')) {
|
||||||
const parts = data.replace('canti:', '').split(':');
|
const parts = data.replace('canti:', '').split(':');
|
||||||
let idsStr = '';
|
let idsStr = '';
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { LiturgiaPage } from './liturgia.page';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild([
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: LiturgiaPage
|
|
||||||
}
|
|
||||||
])
|
|
||||||
],
|
|
||||||
declarations: [LiturgiaPage]
|
|
||||||
})
|
|
||||||
export class LiturgiaPageModule {}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<ion-header [translucent]="true" class="ion-no-border">
|
|
||||||
<ion-toolbar class="bg-gradient">
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button defaultHref="/settings" color="secondary"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title class="outfit-font">Liturgia del Giorno</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content [fullscreen]="true" class="bg-gradient">
|
|
||||||
<div class="liturgia-container ion-padding">
|
|
||||||
|
|
||||||
<div class="date-header-wrapper ion-margin-bottom">
|
|
||||||
<div class="date-header glass">
|
|
||||||
<div class="date-navigation">
|
|
||||||
<ion-button fill="clear" color="secondary" (click)="liturgyService.prevDay()">
|
|
||||||
<ion-icon name="chevron-back-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<h2 class="outfit-font">{{ getTodayString() }}</h2>
|
|
||||||
<ion-button fill="clear" color="secondary" (click)="liturgyService.nextDay()">
|
|
||||||
<ion-icon name="chevron-forward-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="liturgyService.loading()" class="ion-text-center ion-padding">
|
|
||||||
<ion-spinner name="crescent" color="secondary"></ion-spinner>
|
|
||||||
<p class="outfit-font">Caricamento letture...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="!liturgyService.loading() && liturgyService.readings() as r" class="readings-content">
|
|
||||||
|
|
||||||
<ion-accordion-group class="glass-accordion ion-margin-bottom">
|
|
||||||
|
|
||||||
<!-- Prima Lettura -->
|
|
||||||
<ion-accordion value="prima" *ngIf="r.primaLettura">
|
|
||||||
<ion-item slot="header" class="transparent-item">
|
|
||||||
<ion-label class="outfit-font reading-title">Prima Lettura</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div class="reading-text ion-padding" slot="content" [innerHTML]="r.primaLettura"></div>
|
|
||||||
</ion-accordion>
|
|
||||||
|
|
||||||
<!-- Seconda Lettura -->
|
|
||||||
<ion-accordion value="seconda" *ngIf="r.secondaLettura">
|
|
||||||
<ion-item slot="header" class="transparent-item">
|
|
||||||
<ion-label class="outfit-font reading-title">Seconda Lettura</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div class="reading-text ion-padding" slot="content" [innerHTML]="r.secondaLettura"></div>
|
|
||||||
</ion-accordion>
|
|
||||||
|
|
||||||
<!-- Vangelo -->
|
|
||||||
<ion-accordion value="vangelo" *ngIf="r.vangelo">
|
|
||||||
<ion-item slot="header" class="transparent-item">
|
|
||||||
<ion-label class="outfit-font reading-title">Vangelo</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div class="reading-text ion-padding" slot="content" [innerHTML]="r.vangelo"></div>
|
|
||||||
</ion-accordion>
|
|
||||||
|
|
||||||
</ion-accordion-group>
|
|
||||||
|
|
||||||
<!-- Le sezioni dei suggerimenti sono state rimosse temporaneamente in attesa dell'implementazione AI lato server -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="!liturgyService.loading() && !liturgyService.readings()" class="ion-text-center ion-padding glass">
|
|
||||||
<p class="outfit-font">Non è stato possibile caricare le letture per oggi.</p>
|
|
||||||
<ion-button fill="clear" color="secondary" (click)="liturgyService.fetchTodayLiturgy()">Riprova</ion-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</ion-content>
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
.bg-gradient {
|
|
||||||
--background: transparent;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.liturgia-container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.date-navigation {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--ion-color-secondary);
|
|
||||||
text-transform: capitalize;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keywords-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-group {
|
|
||||||
.moment-header {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
ion-badge {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-accordion {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
ion-accordion {
|
|
||||||
--background: transparent;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reading-title {
|
|
||||||
color: var(--ion-color-secondary);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reading-text {
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 1rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
::ng-deep br {
|
|
||||||
display: block;
|
|
||||||
content: "";
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-card {
|
|
||||||
padding: 0;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-title {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-author {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transparent-item {
|
|
||||||
--background: transparent;
|
|
||||||
--color: #fff;
|
|
||||||
--padding-start: 16px;
|
|
||||||
--padding-end: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alto Contrasto per Liturgia */
|
|
||||||
:host-context(body.high-contrast) {
|
|
||||||
.glass-accordion {
|
|
||||||
// Mantengo il look glass per gli header come richiesto
|
|
||||||
|
|
||||||
ion-accordion {
|
|
||||||
.reading-text {
|
|
||||||
color: #000 !important; // Testo nero
|
|
||||||
background: #fff !important; // Sfondo bianco solido per massimo contrasto
|
|
||||||
padding: 20px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
|
||||||
import { LiturgyService } from '../../services/liturgy.service';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { Canto } from '../../services/canti.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-liturgia',
|
|
||||||
templateUrl: './liturgia.page.html',
|
|
||||||
styleUrls: ['./liturgia.page.scss'],
|
|
||||||
standalone: false
|
|
||||||
})
|
|
||||||
export class LiturgiaPage implements OnInit {
|
|
||||||
public liturgyService = inject(LiturgyService);
|
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.liturgyService.fetchTodayLiturgy();
|
|
||||||
}
|
|
||||||
|
|
||||||
suggest() {
|
|
||||||
this.liturgyService.suggestSongs();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDateChange(event: any) {
|
|
||||||
const newDate = event.detail.value.split('T')[0];
|
|
||||||
this.liturgyService.fetchTodayLiturgy(newDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
openSong(song: Canto) {
|
|
||||||
this.router.navigate(['/player'], { queryParams: { id: song.id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
getTodayString(): string {
|
|
||||||
const date = new Date(this.liturgyService.selectedDate());
|
|
||||||
return date.toLocaleDateString('it-IT', {
|
|
||||||
weekday: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<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">
|
||||||
<ion-buttons slot="start">
|
<ion-buttons slot="end">
|
||||||
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
||||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
<ion-icon name="cloud-offline-outline"></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<!-- Youtube Link -->
|
<!-- Youtube Link -->
|
||||||
<ion-button *ngIf="canto()?.link_youtube && canto()?.link_youtube!.length > 5" fill="clear" (click)="openYoutube()">
|
<ion-button *ngIf="connectivityService.isOnline() && canto()?.link_youtube && canto()?.link_youtube!.length > 5" fill="clear" (click)="openYoutube()">
|
||||||
<ion-icon name="logo-youtube" color="danger"></ion-icon>
|
<ion-icon name="logo-youtube" color="danger"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
<ion-footer class="ion-no-border">
|
<ion-footer class="ion-no-border">
|
||||||
<!-- Audio Player Toolbar -->
|
<!-- Audio Player Toolbar -->
|
||||||
<ion-toolbar class="global-player-toolbar glass" *ngIf="canto()?.link_youtube && canto()?.link_youtube!.length > 5 && !settingsService.fullscreenMode()">
|
<ion-toolbar class="global-player-toolbar glass" *ngIf="youtubePlayerService.isPlayerSupported() && canto()?.link_youtube && canto()?.link_youtube!.length > 5 && !settingsService.fullscreenMode()">
|
||||||
<div class="player-content">
|
<div class="player-content">
|
||||||
<div class="controls-row">
|
<div class="controls-row">
|
||||||
<ion-button fill="clear" color="secondary" (click)="prevSong()" class="skip-btn">
|
<ion-button fill="clear" color="secondary" (click)="prevSong()" class="skip-btn">
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="group">
|
<div class="group" *ngIf="connectivityService.isOnline()">
|
||||||
<ion-button *ngIf="canto()?.link_youtube && canto()?.link_youtube!.length > 5" fill="clear" size="small" (click)="openYoutube()">
|
<ion-button *ngIf="canto()?.link_youtube && canto()?.link_youtube!.length > 5" fill="clear" size="small" (click)="openYoutube()">
|
||||||
<ion-icon slot="icon-only" name="logo-youtube" color="danger"></ion-icon>
|
<ion-icon slot="icon-only" name="logo-youtube" color="danger"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -208,19 +208,31 @@
|
|||||||
|
|
||||||
.slim-controls {
|
.slim-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 8px;
|
padding: 4px 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
gap: 16px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mic-btn-portrait {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-indicator {
|
.page-indicator {
|
||||||
|
|||||||
@@ -447,6 +447,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initPlayer(id: string) {
|
private initPlayer(id: string) {
|
||||||
|
if (!this.youtubePlayerService.isPlayerSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const startT = this.initialStartTime;
|
const startT = this.initialStartTime;
|
||||||
this.initialStartTime = 0; // Reset
|
this.initialStartTime = 0; // Reset
|
||||||
|
|
||||||
|
|||||||
@@ -59,13 +59,37 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-toggle slot="end" [checked]="settingsService.showEditor()" (ionChange)="settingsService.toggleEditor()" color="secondary" [disabled]="true"></ion-toggle>
|
<ion-toggle slot="end" [checked]="settingsService.showEditor()" (ionChange)="settingsService.toggleEditor()" color="secondary" [disabled]="true"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="transparent-item" lines="none" routerLink="/liturgia" detail="true" button>
|
</div>
|
||||||
<ion-icon name="book-outline" slot="start" color="secondary"></ion-icon>
|
|
||||||
<ion-label class="outfit-font">
|
<!-- Messa di Riferimento Selection -->
|
||||||
<h2 class="settings-item-title">Liturgia del Giorno</h2>
|
<div class="settings-group glass ion-margin-bottom">
|
||||||
<p class="settings-item-subtitle">Letture e suggerimenti canti</p>
|
<div class="group-header ion-padding-start ion-padding-top">
|
||||||
</ion-label>
|
<h2 class="outfit-font settings-group-title">
|
||||||
</ion-item>
|
Messa di Riferimento (Suggeriti)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="select-item-container ion-padding-horizontal ion-padding-bottom">
|
||||||
|
<div class="select-label-row">
|
||||||
|
<ion-icon name="calendar-outline" color="secondary" class="select-icon"></ion-icon>
|
||||||
|
<div class="select-text-group">
|
||||||
|
<h2 class="settings-item-title outfit-font">Seleziona Messa</h2>
|
||||||
|
<p class="settings-item-subtitle outfit-font">Scegli la messa per i canti consigliati</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="select-box-wrapper">
|
||||||
|
<ion-select [value]="cantiLettureService.selectedMassDate()"
|
||||||
|
(ionChange)="onMassChange($event)"
|
||||||
|
interface="action-sheet"
|
||||||
|
class="custom-select-block"
|
||||||
|
placeholder="Seleziona una messa...">
|
||||||
|
<ion-select-option *ngFor="let mass of cantiLettureService.availableMasses()" [value]="mass.date">
|
||||||
|
{{ mass.formattedLabel }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display Mode Section -->
|
<!-- Display Mode Section -->
|
||||||
@@ -123,6 +147,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Legenda Pulsanti Canto -->
|
||||||
|
<div class="settings-group glass ion-margin-top">
|
||||||
|
<div class="group-header ion-padding-start ion-padding-top">
|
||||||
|
<h2 class="outfit-font settings-group-title">
|
||||||
|
Legenda Pulsanti del Canto
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend-content ion-padding-horizontal ion-padding-bottom">
|
||||||
|
<p class="legend-intro outfit-font">
|
||||||
|
All'interno della schermata di lettura del canto trovi vari pulsanti per controllare la visualizzazione e lo scorrimento:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="legend-list">
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-icon-wrapper">
|
||||||
|
<ion-icon name="mic" color="danger"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="legend-text">
|
||||||
|
<h4 class="outfit-font">Scroll Acustico (Karaoke)</h4>
|
||||||
|
<p class="outfit-font">
|
||||||
|
Attiva lo scorrimento vocale intelligente. L'app ascolta il canto o lo strumento e fa scorrere testo e accordi a tempo di musica, senza bisogno di toccare lo schermo. Uno slider verticale permette di regolare la sensibilità.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-icon-wrapper">
|
||||||
|
<ion-icon name="musical-note" color="secondary"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="legend-text">
|
||||||
|
<h4 class="outfit-font">Trasposizione Tonalità (+ / -)</h4>
|
||||||
|
<p class="outfit-font">
|
||||||
|
Disponibile in modalità "Accordi". Alza o abbassa la tonalità del canto di semitono in semitono, ricalcolando gli accordi all'istante.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-icon-wrapper">
|
||||||
|
<ion-icon name="arrow-up-circle" color="secondary"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="legend-text">
|
||||||
|
<h4 class="outfit-font">Riavvia Canto</h4>
|
||||||
|
<p class="outfit-font">
|
||||||
|
Riporta la visualizzazione all'inizio del testo e azzera il tracciamento vocale dello scroll acustico.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-icon-wrapper">
|
||||||
|
<ion-icon name="chevron-back" color="secondary"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="legend-text">
|
||||||
|
<h4 class="outfit-font">Navigazione Righe</h4>
|
||||||
|
<p class="outfit-font">
|
||||||
|
Permette di avanzare o tornare indietro manualmente tra le righe del testo del canto.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-icon-wrapper">
|
||||||
|
<ion-icon name="add-circle-outline" color="secondary"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="legend-text">
|
||||||
|
<h4 class="outfit-font">Zoom Caratteri</h4>
|
||||||
|
<p class="outfit-font">
|
||||||
|
Ingrandisce o rimpicciolisce il testo e gli accordi (puoi usare anche il gesto "pinch-to-zoom" sullo schermo).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-icon-wrapper">
|
||||||
|
<ion-icon name="logo-youtube" color="danger"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div class="legend-text">
|
||||||
|
<h4 class="outfit-font">YouTube Esterno</h4>
|
||||||
|
<p class="outfit-font">
|
||||||
|
Apre la traccia o il video ufficiale del canto direttamente sull'applicazione esterna di YouTube (quando online).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="credits-footer ion-padding-top ion-text-center">
|
<div class="credits-footer ion-padding-top ion-text-center">
|
||||||
<p class="outfit-font email-text">
|
<p class="outfit-font email-text">
|
||||||
info@canticristiani.it
|
info@canticristiani.it
|
||||||
|
|||||||
@@ -176,3 +176,151 @@ ion-radio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-item-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-text-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.settings-item-title {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-subtitle {
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
margin-top: 2px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-box-wrapper {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-block {
|
||||||
|
--placeholder-color: rgba(255, 255, 255, 0.4);
|
||||||
|
--placeholder-opacity: 1;
|
||||||
|
--color: var(--ion-color-secondary);
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
|
||||||
|
&::part(placeholder), &::part(text) {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body.high-contrast) {
|
||||||
|
.select-box-wrapper {
|
||||||
|
background: rgba(0, 0, 0, 0.05) !important;
|
||||||
|
border-color: rgba(0, 0, 0, 0.3) !important;
|
||||||
|
}
|
||||||
|
.custom-select-block {
|
||||||
|
--color: black !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
.legend-content {
|
||||||
|
.legend-intro {
|
||||||
|
color: #333333 !important;
|
||||||
|
}
|
||||||
|
.legend-item {
|
||||||
|
.legend-icon-wrapper {
|
||||||
|
background: rgba(0, 0, 0, 0.05) !important;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
.legend-text {
|
||||||
|
h4 {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: #222222 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-content {
|
||||||
|
.legend-intro {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.legend-icon-wrapper {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { PlaylistService } from '../../services/playlist.service';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { MyCantiService } from '../../services/my-canti.service';
|
import { MyCantiService } from '../../services/my-canti.service';
|
||||||
|
import { CantiLettureService } from '../../services/canti-letture.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@@ -24,6 +25,7 @@ export class SettingsPage {
|
|||||||
public cantiService = inject(CantiService);
|
public cantiService = inject(CantiService);
|
||||||
public connectivityService = inject(ConnectivityService);
|
public connectivityService = inject(ConnectivityService);
|
||||||
public playlistService = inject(PlaylistService);
|
public playlistService = inject(PlaylistService);
|
||||||
|
public cantiLettureService = inject(CantiLettureService);
|
||||||
private swUpdate = inject(SwUpdate);
|
private swUpdate = inject(SwUpdate);
|
||||||
private toastCtrl = inject(ToastController);
|
private toastCtrl = inject(ToastController);
|
||||||
private modalCtrl = inject(ModalController);
|
private modalCtrl = inject(ModalController);
|
||||||
@@ -38,6 +40,10 @@ export class SettingsPage {
|
|||||||
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMassChange(event: any) {
|
||||||
|
this.cantiLettureService.setSelectedMass(event.detail.value);
|
||||||
|
}
|
||||||
|
|
||||||
async fullRefresh() {
|
async fullRefresh() {
|
||||||
// 1. Refresh JSON data
|
// 1. Refresh JSON data
|
||||||
this.cantiService.refresh();
|
this.cantiService.refresh();
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { Injectable, signal, inject, effect } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Storage } from '@ionic/storage-angular';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
export interface Suggestion {
|
||||||
|
id_canto: number;
|
||||||
|
peso: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MassDetails {
|
||||||
|
title: string;
|
||||||
|
day: string;
|
||||||
|
summary?: string;
|
||||||
|
suggestions: { [moment: string]: Suggestion[] } | Suggestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CantiLettureData {
|
||||||
|
generated_at: string;
|
||||||
|
week_start: string;
|
||||||
|
week_end: string;
|
||||||
|
masses: { [date: string]: MassDetails };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableMass {
|
||||||
|
date: string;
|
||||||
|
title: string;
|
||||||
|
day: string;
|
||||||
|
formattedLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CantiLettureService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private storage = inject(Storage);
|
||||||
|
|
||||||
|
private _storage: Storage | null = null;
|
||||||
|
public data = signal<CantiLettureData | null>(null);
|
||||||
|
public selectedMassDate = signal<string | null>(null);
|
||||||
|
public availableMasses = signal<AvailableMass[]>([]);
|
||||||
|
public suggestionsMap = signal<Map<number, number>>(new Map()); // id_canto -> peso
|
||||||
|
public suggestionsMomentsMap = signal<Map<number, string[]>>(new Map()); // id_canto -> moments[]
|
||||||
|
|
||||||
|
private JSON_URL = 'http://185.193.67.105:3000/cantiletture.json';
|
||||||
|
private SECURE_JSON_URL = '/api/cantiletture.json';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
// Effect to auto-update suggestions maps when selectedMassDate or data changes
|
||||||
|
effect(() => {
|
||||||
|
const activeDate = this.selectedMassDate();
|
||||||
|
const currentData = this.data();
|
||||||
|
const newMap = new Map<number, number>();
|
||||||
|
const newMomentsMap = new Map<number, string[]>();
|
||||||
|
|
||||||
|
if (activeDate && currentData && currentData.masses[activeDate]) {
|
||||||
|
const mass = currentData.masses[activeDate];
|
||||||
|
if (mass.suggestions) {
|
||||||
|
if (Array.isArray(mass.suggestions)) {
|
||||||
|
// Legacy flat suggestions format
|
||||||
|
mass.suggestions.forEach(s => {
|
||||||
|
if (s && s.id_canto) {
|
||||||
|
newMap.set(s.id_canto, s.peso);
|
||||||
|
newMomentsMap.set(s.id_canto, ['Generale']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// New moment-grouped suggestions format
|
||||||
|
Object.keys(mass.suggestions).forEach(moment => {
|
||||||
|
const momentSuggestions = (mass.suggestions as any)[moment];
|
||||||
|
if (Array.isArray(momentSuggestions)) {
|
||||||
|
momentSuggestions.forEach(s => {
|
||||||
|
if (s && s.id_canto) {
|
||||||
|
// Save max weight
|
||||||
|
const existingWeight = newMap.get(s.id_canto) || 0;
|
||||||
|
if (s.peso > existingWeight) {
|
||||||
|
newMap.set(s.id_canto, s.peso);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save liturgical moment
|
||||||
|
const existingMoments = newMomentsMap.get(s.id_canto) || [];
|
||||||
|
if (!existingMoments.includes(moment)) {
|
||||||
|
newMomentsMap.set(s.id_canto, [...existingMoments, moment]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.suggestionsMap.set(newMap);
|
||||||
|
this.suggestionsMomentsMap.set(newMomentsMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Effect to persist selectedMassDate to local storage
|
||||||
|
effect(() => {
|
||||||
|
const activeDate = this.selectedMassDate();
|
||||||
|
if (activeDate) {
|
||||||
|
localStorage.setItem('selected-mass-date', activeDate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// We assume CantiService has already created the storage, but to be safe:
|
||||||
|
this._storage = await this.storage.create();
|
||||||
|
|
||||||
|
// Load cached values
|
||||||
|
const cachedData = await this._storage?.get('cantiletture_data');
|
||||||
|
if (cachedData) {
|
||||||
|
this.processData(cachedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedDate = localStorage.getItem('selected-mass-date');
|
||||||
|
if (savedDate) {
|
||||||
|
this.selectedMassDate.set(savedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh data
|
||||||
|
await this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData() {
|
||||||
|
try {
|
||||||
|
let fetchedData: CantiLettureData | null = null;
|
||||||
|
|
||||||
|
const isProduction = window.location.hostname.includes('canticristiani.it');
|
||||||
|
const primaryUrl = isProduction ? `${window.location.origin}${this.SECURE_JSON_URL}` : this.JSON_URL;
|
||||||
|
const fallbackUrl = isProduction ? this.JSON_URL : `${window.location.origin}${this.SECURE_JSON_URL}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Fetching mass data from primary URL:', primaryUrl);
|
||||||
|
fetchedData = await firstValueFrom(this.http.get<CantiLettureData>(`${primaryUrl}?t=${Date.now()}`));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Primary fetch failed, trying fallback URL...', fallbackUrl, err);
|
||||||
|
try {
|
||||||
|
fetchedData = await firstValueFrom(this.http.get<CantiLettureData>(`${fallbackUrl}?t=${Date.now()}`));
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
console.error('Fallback fetch also failed:', fallbackErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchedData && fetchedData.masses) {
|
||||||
|
this.processData(fetchedData);
|
||||||
|
await this._storage?.set('cantiletture_data', fetchedData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch cantiletture.json from both primary and fallback URLs', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processData(json: CantiLettureData) {
|
||||||
|
this.data.set(json);
|
||||||
|
|
||||||
|
// Build available masses array
|
||||||
|
const massesList: AvailableMass[] = [];
|
||||||
|
const keys = Object.keys(json.masses).sort(); // Sort chronological
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
const mass = json.masses[key];
|
||||||
|
const parts = key.split('-');
|
||||||
|
let formattedLabel = key;
|
||||||
|
if (parts.length === 3) {
|
||||||
|
formattedLabel = `${parts[2]}/${parts[1]}`;
|
||||||
|
}
|
||||||
|
const day = mass.day || '';
|
||||||
|
const title = mass.title || '';
|
||||||
|
|
||||||
|
massesList.push({
|
||||||
|
date: key,
|
||||||
|
title: title,
|
||||||
|
day: day,
|
||||||
|
formattedLabel: `${day} ${formattedLabel}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.availableMasses.set(massesList);
|
||||||
|
|
||||||
|
// If no mass is currently selected, select the first one (or today's mass if exists)
|
||||||
|
if (!this.selectedMassDate() && massesList.length > 0) {
|
||||||
|
// Try to find today's date
|
||||||
|
const todayStr = new Date().toISOString().split('T')[0];
|
||||||
|
const match = massesList.find(m => m.date === todayStr);
|
||||||
|
if (match) {
|
||||||
|
this.selectedMassDate.set(match.date);
|
||||||
|
} else {
|
||||||
|
// Find the first date that is today or in the future
|
||||||
|
const futureMatch = massesList.find(m => m.date >= todayStr);
|
||||||
|
if (futureMatch) {
|
||||||
|
this.selectedMassDate.set(futureMatch.date);
|
||||||
|
} else {
|
||||||
|
this.selectedMassDate.set(massesList[0].date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedMass(date: string) {
|
||||||
|
this.selectedMassDate.set(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,11 @@ export interface Indice {
|
|||||||
type: 'liturgico' | 'tematico';
|
type: 'liturgico' | 'tematico';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CantoEseguito {
|
||||||
|
id_canti: number;
|
||||||
|
num: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@@ -31,6 +36,7 @@ export class CantiService {
|
|||||||
public canti = signal<Canto[]>([]);
|
public canti = signal<Canto[]>([]);
|
||||||
public indiceLiturgico = signal<Indice[]>([]);
|
public indiceLiturgico = signal<Indice[]>([]);
|
||||||
public indiceTematico = signal<Indice[]>([]);
|
public indiceTematico = signal<Indice[]>([]);
|
||||||
|
public cantiEseguiti = signal<CantoEseguito[]>([]);
|
||||||
// Keep momenti for backward compatibility or temporary usage
|
// Keep momenti for backward compatibility or temporary usage
|
||||||
public momenti = signal<Indice[]>([]);
|
public momenti = signal<Indice[]>([]);
|
||||||
public loading = signal<boolean>(false);
|
public loading = signal<boolean>(false);
|
||||||
@@ -53,9 +59,11 @@ export class CantiService {
|
|||||||
const cachedCanti = await this._storage?.get('canti');
|
const cachedCanti = await this._storage?.get('canti');
|
||||||
const cachedLit = await this._storage?.get('indiceLiturgico');
|
const cachedLit = await this._storage?.get('indiceLiturgico');
|
||||||
const cachedTem = await this._storage?.get('indiceTematico');
|
const cachedTem = await this._storage?.get('indiceTematico');
|
||||||
|
const cachedEseguiti = await this._storage?.get('cantiEseguiti');
|
||||||
if (cachedCanti) this.canti.set(cachedCanti);
|
if (cachedCanti) this.canti.set(cachedCanti);
|
||||||
if (cachedLit) this.indiceLiturgico.set(cachedLit);
|
if (cachedLit) this.indiceLiturgico.set(cachedLit);
|
||||||
if (cachedTem) this.indiceTematico.set(cachedTem);
|
if (cachedTem) this.indiceTematico.set(cachedTem);
|
||||||
|
if (cachedEseguiti) this.cantiEseguiti.set(cachedEseguiti);
|
||||||
// Sync momenti
|
// Sync momenti
|
||||||
if (cachedLit) this.momenti.set(cachedLit);
|
if (cachedLit) this.momenti.set(cachedLit);
|
||||||
}
|
}
|
||||||
@@ -106,14 +114,18 @@ export class CantiService {
|
|||||||
id_momenti: pivotMap.get(c.id_canti) || []
|
id_momenti: pivotMap.get(c.id_canti) || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const eseguitiData = response.canti_eseguiti?.data || [];
|
||||||
|
|
||||||
this.canti.set(linkedCanti);
|
this.canti.set(linkedCanti);
|
||||||
this.indiceLiturgico.set(litData);
|
this.indiceLiturgico.set(litData);
|
||||||
this.indiceTematico.set(temData);
|
this.indiceTematico.set(temData);
|
||||||
this.momenti.set(litData); // Fallback
|
this.momenti.set(litData); // Fallback
|
||||||
|
this.cantiEseguiti.set(eseguitiData);
|
||||||
|
|
||||||
await this._storage?.set('canti', linkedCanti);
|
await this._storage?.set('canti', linkedCanti);
|
||||||
await this._storage?.set('indiceLiturgico', litData);
|
await this._storage?.set('indiceLiturgico', litData);
|
||||||
await this._storage?.set('indiceTematico', temData);
|
await this._storage?.set('indiceTematico', temData);
|
||||||
|
await this._storage?.set('cantiEseguiti', eseguitiData);
|
||||||
}
|
}
|
||||||
this.progress.set(100);
|
this.progress.set(100);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
import { Injectable, inject, signal } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { CantiService, Canto } from './canti.service';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
export interface LiturgyReadings {
|
|
||||||
date: string;
|
|
||||||
primaLettura?: string;
|
|
||||||
salmo?: string;
|
|
||||||
secondaLettura?: string;
|
|
||||||
vangelo?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class LiturgyService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private cantiService = inject(CantiService);
|
|
||||||
|
|
||||||
public readings = signal<LiturgyReadings | null>(null);
|
|
||||||
public suggestions = signal<Canto[]>([]);
|
|
||||||
public loading = signal<boolean>(false);
|
|
||||||
|
|
||||||
public selectedDate = signal<string>(this.getNextSunday());
|
|
||||||
public keywords = signal<string[]>([]);
|
|
||||||
public groupedSuggestions = signal<{moment: string, canti: Canto[]}[]>([]);
|
|
||||||
|
|
||||||
private PROXY_URL = 'https://corsproxy.io/?';
|
|
||||||
private RSS_URL = 'https://liturgia.silvestrini.org/rss';
|
|
||||||
|
|
||||||
private getNextSunday(): string {
|
|
||||||
const d = new Date();
|
|
||||||
d.setDate(d.getDate() + (7 - d.getDay()) % 7);
|
|
||||||
if (d.getDay() === 0 && new Date().getDay() === 0) {
|
|
||||||
// Keep today if today is Sunday
|
|
||||||
} else if (d.getDay() === 0) {
|
|
||||||
// already sunday
|
|
||||||
} else {
|
|
||||||
d.setDate(d.getDate() + (7 - d.getDay()));
|
|
||||||
}
|
|
||||||
return d.toISOString().split('T')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
prevDay() {
|
|
||||||
const d = new Date(this.selectedDate());
|
|
||||||
d.setDate(d.getDate() - 1);
|
|
||||||
this.fetchTodayLiturgy(d.toISOString().split('T')[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextDay() {
|
|
||||||
const d = new Date(this.selectedDate());
|
|
||||||
d.setDate(d.getDate() + 1);
|
|
||||||
this.fetchTodayLiturgy(d.toISOString().split('T')[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchTodayLiturgy(date?: string) {
|
|
||||||
if (date) this.selectedDate.set(date);
|
|
||||||
const targetDate = this.selectedDate();
|
|
||||||
|
|
||||||
this.loading.set(true);
|
|
||||||
this.suggestions.set([]);
|
|
||||||
this.groupedSuggestions.set([]);
|
|
||||||
this.keywords.set([]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let xmlText = '';
|
|
||||||
try {
|
|
||||||
xmlText = await firstValueFrom(this.http.get(this.PROXY_URL + encodeURIComponent(this.RSS_URL), { responseType: 'text' }));
|
|
||||||
} catch (e) {
|
|
||||||
const fallbackProxy = 'https://api.allorigins.win/raw?url=';
|
|
||||||
xmlText = await firstValueFrom(this.http.get(fallbackProxy + encodeURIComponent(this.RSS_URL), { responseType: 'text' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
|
||||||
const items = Array.from(xmlDoc.querySelectorAll('item'));
|
|
||||||
|
|
||||||
const lettureItem = items.find(item => {
|
|
||||||
const title = item.querySelector('title')?.textContent || '';
|
|
||||||
const category = item.querySelector('category')?.textContent || '';
|
|
||||||
return title.startsWith(targetDate) && category === 'letture';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lettureItem) {
|
|
||||||
const description = lettureItem.querySelector('description')?.textContent || '';
|
|
||||||
this.readings.set(this.parseDescription(description, targetDate));
|
|
||||||
} else {
|
|
||||||
this.readings.set(null);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch liturgy', err);
|
|
||||||
this.readings.set(null);
|
|
||||||
} finally {
|
|
||||||
this.loading.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseDescription(html: string, date: string): LiturgyReadings {
|
|
||||||
const readings: LiturgyReadings = { date };
|
|
||||||
|
|
||||||
const parts = html.split(/([A-Z\s]+:)/);
|
|
||||||
|
|
||||||
let currentLabel = '';
|
|
||||||
for (const part of parts) {
|
|
||||||
const trimmed = part.trim();
|
|
||||||
if (trimmed.endsWith(':')) {
|
|
||||||
currentLabel = trimmed.toUpperCase();
|
|
||||||
} else if (trimmed) {
|
|
||||||
if (currentLabel.includes('PRIMA LETTURA')) readings.primaLettura = trimmed;
|
|
||||||
else if (currentLabel.includes('SALMO')) readings.salmo = trimmed;
|
|
||||||
else if (currentLabel.includes('SECONDA LETTURA')) readings.secondaLettura = trimmed;
|
|
||||||
else if (currentLabel.includes('VANGELO')) readings.vangelo = trimmed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return readings;
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestSongs() {
|
|
||||||
const currentReadings = this.readings();
|
|
||||||
if (!currentReadings) return;
|
|
||||||
|
|
||||||
const allText = [
|
|
||||||
currentReadings.primaLettura,
|
|
||||||
currentReadings.salmo,
|
|
||||||
currentReadings.secondaLettura,
|
|
||||||
currentReadings.vangelo
|
|
||||||
].join(' ').toLowerCase();
|
|
||||||
|
|
||||||
const stopWords = new Set(['il', 'lo', 'la', 'i', 'gli', 'le', 'di', 'a', 'da', 'in', 'con', 'su', 'per', 'tra', 'fra', 'e', 'o', 'che', 'non', 'si', 'del', 'al', 'dal', 'nel', 'col', 'sul', 'mio', 'tuo', 'suo', 'noi', 'voi', 'loro', 'mio', 'mia', 'miei', 'mie']);
|
|
||||||
const words = allText.match(/\b(\w+)\b/g) || [];
|
|
||||||
const keywords = Array.from(new Set(words.filter(w => w.length > 3 && !stopWords.has(w))));
|
|
||||||
this.keywords.set(keywords.slice(0, 20));
|
|
||||||
|
|
||||||
const songs = this.cantiService.canti();
|
|
||||||
const scoredSongs = songs.map(song => {
|
|
||||||
let score = 0;
|
|
||||||
const titleLower = song.titolo.toLowerCase();
|
|
||||||
const lyricsLower = (song.testo || '').toLowerCase();
|
|
||||||
|
|
||||||
keywords.forEach(kw => {
|
|
||||||
if (titleLower.includes(kw)) score += 10;
|
|
||||||
const regex = new RegExp(`\\b${kw}\\b`, 'g');
|
|
||||||
const matches = lyricsLower.match(regex);
|
|
||||||
if (matches) score += matches.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { song, score };
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = scoredSongs
|
|
||||||
.filter(s => s.score > 0)
|
|
||||||
.sort((a, b) => b.score - a.score);
|
|
||||||
|
|
||||||
const moments = this.cantiService.indiceLiturgico();
|
|
||||||
const grouped: {moment: string, canti: Canto[]}[] = [];
|
|
||||||
|
|
||||||
moments.forEach(m => {
|
|
||||||
const cantiInMoment = results
|
|
||||||
.filter(s => s.song.id_momenti?.includes(m.id))
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(s => s.song);
|
|
||||||
|
|
||||||
if (cantiInMoment.length > 0) {
|
|
||||||
grouped.push({ moment: m.tag_name, canti: cantiInMoment });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.groupedSuggestions.set(grouped);
|
|
||||||
this.suggestions.set(results.slice(0, 20).map(s => s.song));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,14 @@ import { Injectable, signal, effect } from '@angular/core';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
public highContrast = signal<boolean>(false);
|
public highContrast = signal<boolean>(true);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Load from localStorage
|
// Load from localStorage
|
||||||
const saved = localStorage.getItem('high-contrast');
|
const saved = localStorage.getItem('high-contrast');
|
||||||
if (saved === 'true') {
|
if (saved === 'false') {
|
||||||
|
this.highContrast.set(false);
|
||||||
|
} else {
|
||||||
this.highContrast.set(true);
|
this.highContrast.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Injectable, signal, effect, inject } from '@angular/core';
|
import { Injectable, signal, effect, inject, computed } from '@angular/core';
|
||||||
import { CantiService } from './canti.service';
|
import { CantiService } from './canti.service';
|
||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from './settings.service';
|
||||||
import { MediaSessionService } from './media-session.service';
|
import { MediaSessionService } from './media-session.service';
|
||||||
import { MyCantiService } from './my-canti.service';
|
import { MyCantiService } from './my-canti.service';
|
||||||
|
import { ConnectivityService } from './connectivity.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -12,6 +13,21 @@ export class YoutubePlayerService {
|
|||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private mediaSessionService = inject(MediaSessionService);
|
private mediaSessionService = inject(MediaSessionService);
|
||||||
private myCantiService = inject(MyCantiService);
|
private myCantiService = inject(MyCantiService);
|
||||||
|
private connectivityService = inject(ConnectivityService);
|
||||||
|
|
||||||
|
public isPlayerSupported = computed<boolean>(() => {
|
||||||
|
// Check if offline
|
||||||
|
if (!this.connectivityService.isOnline()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if iPhone/iPad/iPod
|
||||||
|
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||||
|
const isIOS = /iphone|ipad|ipod/.test(userAgent);
|
||||||
|
if (isIOS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
public currentCantoId = signal<string | null>(null);
|
public currentCantoId = signal<string | null>(null);
|
||||||
public isPlaying = signal<boolean>(false);
|
public isPlaying = signal<boolean>(false);
|
||||||
@@ -32,6 +48,14 @@ export class YoutubePlayerService {
|
|||||||
this.setupMediaSession();
|
this.setupMediaSession();
|
||||||
this.initSilentAudio();
|
this.initSilentAudio();
|
||||||
this.setupBackgroundPersistence();
|
this.setupBackgroundPersistence();
|
||||||
|
|
||||||
|
// Auto-stop player when not supported (e.g. going offline)
|
||||||
|
effect(() => {
|
||||||
|
const supported = this.isPlayerSupported();
|
||||||
|
if (!supported && this.currentCantoId()) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private initSilentAudio() {
|
private initSilentAudio() {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const VERSION = '2026.05.16.1840';
|
export const VERSION = '2026.05.18.0125';
|
||||||
|
|||||||
@@ -354,3 +354,52 @@ body.high-contrast .offline-thumb {
|
|||||||
.outfit-font {
|
.outfit-font {
|
||||||
font-family: 'Outfit', sans-serif;
|
font-family: 'Outfit', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure high visibility of Action Sheet options in Dark Mode and High Contrast */
|
||||||
|
ion-action-sheet {
|
||||||
|
--background: #1e1e1e !important;
|
||||||
|
--color: #ffffff !important;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #ffffff !important;
|
||||||
|
--color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-sheet-button {
|
||||||
|
color: #ffffff !important;
|
||||||
|
--color: #ffffff !important;
|
||||||
|
|
||||||
|
&.action-sheet-selected {
|
||||||
|
color: var(--ion-color-secondary) !important;
|
||||||
|
--color: var(--ion-color-secondary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-sheet-title {
|
||||||
|
color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.high-contrast ion-action-sheet {
|
||||||
|
--background: #ffffff !important;
|
||||||
|
--color: #000000 !important;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #000000 !important;
|
||||||
|
--color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-sheet-button {
|
||||||
|
color: #000000 !important;
|
||||||
|
--color: #000000 !important;
|
||||||
|
|
||||||
|
&.action-sheet-selected {
|
||||||
|
color: #e67e22 !important;
|
||||||
|
--color: #e67e22 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-sheet-title {
|
||||||
|
color: rgba(0, 0, 0, 0.7) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user