Refactor: Restringimento drag & drop, legenda impostazioni, modalità offline e rimozione sezione liturgia

This commit is contained in:
David Frassi
2026-05-18 01:36:18 +02:00
parent 9982441c3c
commit 961b7ba65c
22 changed files with 988 additions and 500 deletions
+74 -11
View File
@@ -10,10 +10,6 @@
</div>
</ion-title>
<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-icon slot="icon-only" name="qr-code-outline"></ion-icon>
@@ -83,6 +79,17 @@
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
</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
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
@@ -96,6 +103,17 @@
<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>
</div>
</div>
</div>
@@ -108,6 +126,9 @@
<!-- Selection Mode Actions -->
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
<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-icon slot="icon-only" name="save-outline"></ion-icon>
</ion-button>
@@ -162,6 +183,32 @@
(cdkDropListDropped)="drop($event)"
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()"
[id]="'canto-' + canto.id"
class="glass ion-margin-bottom custom-item"
@@ -189,13 +236,23 @@
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary)">
{{ canto.titolo }}
</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>
<!-- Video Thumbnail Section -->
<div *ngIf="canto.link_youtube && canto.link_youtube.length > 5" class="thumb-section" (click)="$event.stopPropagation()">
<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)">
<img [src]="cantiService.getYoutubeThumb(canto.link_youtube)"
class="thumb-img loaded"
@@ -205,11 +262,17 @@
</div>
</div>
</ng-container>
<ng-template #offlineThumb>
<ng-template #noPlayerThumb>
<div class="thumb-wrapper">
<div class="offline-thumb">
<ion-icon name="cloud-offline-outline"></ion-icon>
</div>
<img [src]="cantiService.getYoutubeThumb(canto.link_youtube)"
class="thumb-img loaded"
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>
</ng-template>
</div>
@@ -249,7 +312,7 @@
</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">
<div class="player-content">
<div class="controls-row">
+143 -3
View File
@@ -31,7 +31,7 @@
.search-wrapper-group {
display: flex;
flex-direction: column;
padding: 8px 16px 12px 16px; // Added top padding for extra clearance
padding: 2px 16px 12px 16px; // Reduced top padding
gap: 12px;
}
@@ -174,7 +174,7 @@ ion-title {
align-items: center;
justify-content: flex-start;
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 {
@@ -661,7 +661,7 @@ ion-title {
justify-content: center;
padding: 0 12px 0 0;
cursor: grab;
color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.5);
ion-icon {
font-size: 1.8rem;
@@ -748,3 +748,143 @@ ion-title {
.transparent-list {
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
View File
@@ -13,6 +13,7 @@ import { ModalController, AlertController, ToastController } from '@ionic/angula
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MyCantiService } from '../services/my-canti.service';
import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component';
import { CantiLettureService } from '../services/canti-letture.service';
@Component({
selector: 'app-home',
@@ -25,6 +26,9 @@ export class HomePage implements OnDestroy {
public selectedLiturgico = signal<number | null>(null);
public selectedTematico = signal<number | null>(null);
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 loadedThumbs = new Set<string>();
public version = VERSION;
@@ -49,6 +53,7 @@ export class HomePage implements OnDestroy {
public myCantiService = inject(MyCantiService);
public playlistService = inject(PlaylistService);
public settingsService = inject(SettingsService);
public cantiLettureService = inject(CantiLettureService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private modalCtrl = inject(ModalController);
@@ -93,6 +98,8 @@ export class HomePage implements OnDestroy {
const litId = this.selectedLiturgico();
const temId = this.selectedTematico();
const onlyMine = this.showOnlyMine();
const topTen = this.showTopTen();
const suggeriti = this.showSuggeriti();
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));
}
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)
const activeIds = this.playlistService.activeListIds();
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 (selectionMode && !this.isAddingSongs() && activeIds.length > 0) {
// If we are in selection mode, only show restricted list if NOT adding songs
if (selectionMode && !this.isAddingSongs()) {
return this.reorderList();
}
@@ -124,6 +136,25 @@ export class HomePage implements OnDestroy {
.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;
// 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(() => {
const selectionMode = this.playlistService.selectionMode();
const selectedIds = this.playlistService.selectedIds();
const activeIds = this.playlistService.activeListIds();
if (selectionMode) {
if (activeIds.length === 0 && !this.isAddingSongs()) {
this.isAddingSongs.set(true);
if (!prevSelectionMode) {
if (activeIds.length === 0) {
this.isAddingSongs.set(true);
} else {
this.isAddingSongs.set(false);
}
}
prevSelectionMode = true;
const currentIds = this.reorderList().map(c => c.id);
const newIds = Array.from(selectedIds);
@@ -208,6 +246,7 @@ export class HomePage implements OnDestroy {
this.reorderList.set(updatedList);
}
} else {
prevSelectionMode = false;
this.isAddingSongs.set(false);
}
}, { allowSignalWrites: true });
@@ -267,11 +306,6 @@ export class HomePage implements OnDestroy {
}
goToCanto(id: string) {
if (this.playlistService.selectionMode()) {
this.playlistService.toggleSongSelection(id);
return;
}
const params: any = { id };
const isPlaying = this.youtubePlayerService.isPlaying();
@@ -379,6 +413,8 @@ export class HomePage implements OnDestroy {
return this.selectedLiturgico() !== null ||
this.selectedTematico() !== null ||
this.showOnlyMine() ||
this.showTopTen() ||
this.showSuggeriti() ||
this.playlistService.activeListName() !== null ||
this.searchQuery() !== '';
}
@@ -387,6 +423,8 @@ export class HomePage implements OnDestroy {
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
this.showOnlyMine.set(false);
this.showTopTen.set(false);
this.showSuggeriti.set(false);
this.playlistService.activeListIds.set([]);
this.playlistService.activeListName.set(null);
this.playlistService.activePlaylistId.set(null);
@@ -415,6 +453,73 @@ export class HomePage implements OnDestroy {
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) {
this.loadedThumbs.add(id);
}
@@ -429,6 +534,9 @@ export class HomePage implements OnDestroy {
playVideo(event: Event, id: string) {
event.stopPropagation();
if (!this.youtubePlayerService.isPlayerSupported()) {
return;
}
this.youtubePlayerService.setMediaSessionCallbacks(
() => this.playNextPreview(),
() => this.playPrevPreview()
@@ -577,14 +685,30 @@ export class HomePage implements OnDestroy {
handleScannedData(data: string) {
if (!data) return;
if (data.includes('?import=')) {
if (data.includes('import=')) {
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);
return;
} catch (e) {
console.error('Failed to parse scanned link', e);
}
} else if (data.startsWith('canti:')) {
}
if (data.startsWith('canti:')) {
const parts = data.replace('canti:', '').split(':');
let idsStr = '';
if (parts.length > 1) {