Files
canti/src/app/home/home.page.html
T

658 lines
37 KiB
HTML

<ion-header class="ion-no-border">
<ion-toolbar class="bg-gradient">
<ion-title class="outfit-font">
<div class="header-logo-wrapper">
<img src="assets/icon/favicon.png" class="header-logo">
<div class="header-text-group">
<span class="app-name">{{ appName }}</span>
<span class="version-badge" style="cursor: default; display: inline-flex; align-items: center; gap: 4px;">
v{{ version }}
</span>
</div>
</div>
</ion-title>
<ion-buttons slot="end">
<ion-button routerLink="/propose-canto" class="add-btn" title="Crea un nuovo canto">
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button>
<ng-container *ngIf="hasUpdateAvailable(); else showQrBtn">
<ion-button (click)="checkForAppUpdate($event)" class="add-btn" title="Aggiornamento disponibile">
<ion-icon slot="icon-only" name="refresh-outline" color="secondary"></ion-icon>
</ion-button>
</ng-container>
<ng-template #showQrBtn>
<ion-button (click)="importPlaylist()" class="add-btn">
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
</ion-button>
</ng-template>
<ion-button routerLink="/settings" class="settings-btn">
<ion-icon slot="icon-only" name="settings-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
<ion-toolbar class="bg-gradient" *ngIf="!playlistService.selectionMode() || isAddingSongs()">
<div class="search-wrapper-group">
<div class="search-row">
<div class="search-wrapper glass">
<ion-searchbar
placeholder="Cerca un canto..."
[value]="searchQuery()"
(ionInput)="onSearch($event)"
[disabled]="isAdvancedSearchOpen()"
class="custom-searchbar">
</ion-searchbar>
<ion-button fill="clear" (click)="toggleAdvancedSearch()" class="adv-search-btn" [class.active]="isAdvancedSearchOpen()" title="Ricerca avanzata">
<ion-icon slot="icon-only" name="options-outline" [color]="isAdvancedSearchOpen() ? 'secondary' : 'medium'"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="toggleVoiceSearch('global')" class="voice-search-btn" [disabled]="isAdvancedSearchOpen()">
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'global') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'global') ? 'danger' : 'secondary'"></ion-icon>
</ion-button>
</div>
</div>
<!-- Advanced Search & Filters Toggle Row -->
<div class="advanced-search-toggle-row">
<div class="toggle-row-left">
<div class="song-count-card outfit-font">
{{ filteredCanti().length }}
</div>
<button type="button" class="advanced-search-link outfit-font" (click)="toggleAdvancedSearch()">
<ion-icon [name]="isAdvancedSearchOpen() ? 'chevron-up-outline' : 'options-outline'"></ion-icon>
<span>Ricerche</span>
<span class="inline-clear-btn" *ngIf="hasAdvancedSearchParams()" (click)="clearAdvancedSearch($event)" title="Reset ricerche">
<ion-icon name="close-circle"></ion-icon>
</span>
</button>
<button type="button" class="advanced-search-link outfit-font" (click)="toggleFilterCard()">
<ion-icon [name]="isFilterCardOpen() ? 'chevron-up-outline' : 'filter-outline'"></ion-icon>
<span>Filtri</span>
<span class="inline-clear-btn" *ngIf="hasFilterCardParams()" (click)="clearFilterCard($event)" title="Reset filtri">
<ion-icon name="close-circle"></ion-icon>
</span>
</button>
<button type="button" class="advanced-search-link outfit-font" (click)="togglePlaylistCard()">
<ion-icon [name]="isPlaylistCardOpen() ? 'chevron-up-outline' : 'list-outline'"></ion-icon>
<span>Liste</span>
<span class="inline-clear-btn" *ngIf="hasPlaylistCardParams()" (click)="clearPlaylistCard($event)" title="Reset liste">
<ion-icon name="close-circle"></ion-icon>
</span>
</button>
</div>
</div>
<!-- Advanced Search Panel -->
<div class="advanced-search-panel glass outfit-font"
*ngIf="isAdvancedSearchOpen()"
(touchstart)="onPanelTouchStart($event)"
(touchend)="onPanelTouchEnd($event, 'advancedSearch')">
<div class="adv-search-grid">
<div class="adv-input-group">
<label class="adv-label">Ricerca per titolo</label>
<div class="adv-input-wrapper">
<ion-icon name="text-outline" class="adv-input-icon"></ion-icon>
<input
type="text"
placeholder="Titolo canto..."
[value]="searchTitle()"
(input)="onSearchTitleInput($event)"
class="adv-input" />
<ion-icon name="close-circle" *ngIf="searchTitle()" (click)="searchTitle.set('')" class="adv-clear-icon"></ion-icon>
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('title')" class="adv-mic-btn">
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'title') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'title') ? 'danger' : 'secondary'"></ion-icon>
</ion-button>
</div>
</div>
<div class="adv-input-group">
<label class="adv-label">Ricerca per autore</label>
<div class="adv-input-wrapper">
<ion-icon name="person-outline" class="adv-input-icon"></ion-icon>
<input
type="text"
placeholder="Nome autore..."
[value]="searchAuthor()"
(input)="onSearchAuthorInput($event)"
class="adv-input" />
<ion-icon name="close-circle" *ngIf="searchAuthor()" (click)="searchAuthor.set('')" class="adv-clear-icon"></ion-icon>
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('author')" class="adv-mic-btn">
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'author') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'author') ? 'danger' : 'secondary'"></ion-icon>
</ion-button>
</div>
</div>
<div class="adv-input-group">
<label class="adv-label">Ricerca per testo</label>
<div class="adv-input-wrapper">
<ion-icon name="document-text-outline" class="adv-input-icon"></ion-icon>
<input
type="text"
placeholder="Parole nel testo..."
[value]="searchText()"
(input)="onSearchTextInput($event)"
class="adv-input" />
<ion-icon name="close-circle" *ngIf="searchText()" (click)="searchText.set('')" class="adv-clear-icon"></ion-icon>
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('text')" class="adv-mic-btn">
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'text') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'text') ? 'danger' : 'secondary'"></ion-icon>
</ion-button>
</div>
</div>
<div class="adv-input-group">
<label class="adv-label">Ricerca per nr canto</label>
<div class="adv-input-wrapper">
<ion-icon name="pricetag-outline" class="adv-input-icon"></ion-icon>
<input
type="text"
placeholder="Numero canto..."
[value]="searchNumber()"
(input)="onSearchNumberInput($event)"
class="adv-input" />
<ion-icon name="close-circle" *ngIf="searchNumber()" (click)="searchNumber.set('')" class="adv-clear-icon"></ion-icon>
<ion-button fill="clear" size="small" (click)="toggleVoiceSearch('number')" class="adv-mic-btn">
<ion-icon slot="icon-only" [name]="(audioEngine.isSearching() && activeVoiceField() === 'number') ? 'mic' : 'mic-outline'" [color]="(audioEngine.isSearching() && activeVoiceField() === 'number') ? 'danger' : 'secondary'"></ion-icon>
</ion-button>
</div>
</div>
</div>
</div>
<!-- Playlist Card (default closed) -->
<div class="filter-card glass outfit-font"
*ngIf="isPlaylistCardOpen()"
(touchstart)="onPanelTouchStart($event)"
(touchend)="onPanelTouchEnd($event, 'playlistCard')">
<div class="filter-card-row">
<span class="filter-row-label">Playlist</span>
<div class="filter-row-items">
<!-- Comunità filter button if enabled -->
<div class="comunita-filter-wrapper" style="position: relative; display: inline-flex; align-items: center; z-index: 999;" *ngIf="settingsService.comunitaEnabled()">
<ion-button
[fill]="comunitaService.isFilterActive() ? 'solid' : 'outline'"
size="small"
(click)="toggleComunitaFilter()"
[color]="comunitaService.isFilterActive() ? 'secondary' : 'medium'"
class="filter-chip"
style="margin: 0;">
<ion-icon slot="start" name="people-outline" style="font-size: 1.1rem; margin-right: 4px;"></ion-icon>
{{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }}
</ion-button>
</div>
<!-- Playlist Chips -->
<div
*ngFor="let item of playlistService.allPlaylists()"
class="momento-chip glass"
[class.active]="playlistService.activePlaylistId() === item.id"
[class.comunita-chip]="item.isComunita"
(click)="playlistService.activePlaylistId() === item.id ? clearSpecialList($event) : selectPlaylist(item)">
<ion-icon *ngIf="item.isComunita" name="people-outline" style="font-size: 0.85rem; margin-right: 4px; vertical-align: middle;"></ion-icon>
{{ item.name }}
<span *ngIf="playlistService.activePlaylistId() === item.id && isRemotePlaylist() && playlistService.hasRemotePlaylistUpdate()" (click)="refreshActiveRemotePlaylist(); $event.stopPropagation();" style="display: inline-flex; align-items: center; justify-content: center; padding: 2px; margin-left: 6px; background: rgba(var(--ion-color-secondary-rgb), 0.25); border-radius: 50%; border: 1px solid var(--ion-color-secondary); width: 18px; height: 18px; vertical-align: middle;">
<ion-icon name="refresh-outline" style="font-size: 0.85rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
</span>
<span class="close-icon-wrapper" *ngIf="playlistService.activePlaylistId() === item.id" (click)="clearSpecialList($event)">
<ion-icon name="close-circle"></ion-icon>
</span>
</div>
<span *ngIf="playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()" class="empty-playlist-text">
Nessuna playlist creata
</span>
</div>
</div>
<div class="filter-card-footer" *ngIf="hasPlaylistCardParams()">
<div></div>
<button type="button" class="clear-advanced-link outfit-font" (click)="clearPlaylistCard()">
<ion-icon name="close-circle-outline"></ion-icon>
<span>Reset liste</span>
</button>
</div>
</div>
<!-- Filter Card (default closed) -->
<div class="filter-card glass outfit-font"
*ngIf="isFilterCardOpen()"
(touchstart)="onPanelTouchStart($event)"
(touchend)="onPanelTouchEnd($event, 'filterCard')">
<!-- Row 2: Tipologia -->
<div class="filter-card-row">
<span class="filter-row-label">Tipologia</span>
<div class="filter-row-items">
<div
class="momento-chip glass"
[class.active]="showValidati()"
(click)="toggleValidati()">
Validati
<span class="close-icon-wrapper" *ngIf="showValidati()" (click)="clearListaCompleta($event)">
<ion-icon name="close-circle"></ion-icon>
</span>
</div>
<div
class="momento-chip glass"
[class.active]="showNonValidati()"
(click)="toggleNonValidati()"
style="display: inline-flex; align-items: center; gap: 6px;">
<span>Non Validati</span>
<ion-icon
*ngIf="showNonValidati() && !playlistService.selectionMode() && settingsService.showEditor()"
name="add-outline"
(click)="$event.stopPropagation()"
routerLink="/propose-canto"
class="non-validati-add-icon">
</ion-icon>
<span class="close-icon-wrapper" *ngIf="showNonValidati()" (click)="clearListaCompleta($event)">
<ion-icon name="close-circle"></ion-icon>
</span>
</div>
</div>
</div>
<!-- Row 3: Liturgia -->
<div class="filter-card-row">
<span class="filter-row-label">Liturgia</span>
<div class="filter-row-items">
<div
*ngFor="let item of cantiService.indiceLiturgico()"
class="momento-chip glass"
[class.active]="selectedLiturgico() === item.id"
(click)="toggleIndex(item.id, 'liturgico')">
{{ item.tag_name }}
<span class="close-icon-wrapper" *ngIf="selectedLiturgico() === item.id" (click)="clearLiturgico($event)">
<ion-icon name="close-circle"></ion-icon>
</span>
</div>
</div>
</div>
<!-- Row 4: Periodo -->
<div class="filter-card-row">
<span class="filter-row-label">Periodo</span>
<div class="filter-row-items">
<div
*ngFor="let item of cantiService.indiceTematico()"
class="momento-chip glass"
[class.active]="selectedTematico() === item.id"
(click)="toggleIndex(item.id, 'tematico')">
{{ item.tag_name }}
<span class="close-icon-wrapper" *ngIf="selectedTematico() === item.id" (click)="clearTematico($event)">
<ion-icon name="close-circle"></ion-icon>
</span>
</div>
</div>
</div>
<!-- Row 5 (Footer): Suggeriti, Top Ten & Reset filtri -->
<div class="filter-card-footer">
<div class="footer-actions">
<ion-button
[fill]="showSuggeriti() ? 'solid' : 'outline'"
size="small"
(click)="toggleSuggeriti()"
class="filter-chip">
Suggeriti
<span class="close-icon-wrapper" *ngIf="showSuggeriti()" (click)="clearSuggeriti($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
</ion-button>
<ion-button
[fill]="showTopTen() ? 'solid' : 'outline'"
size="small"
(click)="toggleTopTen()"
class="filter-chip">
Top Ten
<span class="close-icon-wrapper" *ngIf="showTopTen()" (click)="clearTopTen($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
</ion-button>
<ion-button
[fill]="showOnlyMine() ? 'solid' : 'outline'"
size="small"
(click)="toggleOnlyMine()"
class="filter-chip">
Miei
<span class="close-icon-wrapper" *ngIf="showOnlyMine()" (click)="clearOnlyMine($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
</ion-button>
</div>
</div>
</div>
</div>
</ion-toolbar>
<!-- Fixed Actions Bar in header for active playlist or selection mode -->
<ion-toolbar class="bg-gradient playlist-toolbar" *ngIf="(playlistService.activeListName() !== null && !playlistService.selectionMode()) || (playlistService.selectionMode() && reorderList().length > 0)">
<div class="playlist-actions-bar">
<!-- Duration Badge on the left -->
<div class="playlist-duration-pill outfit-font" *ngIf="totalPlaylistDuration()">
<ion-icon name="time-outline"></ion-icon>
<span>{{ totalPlaylistDuration() }}</span>
</div>
<!-- Playlist Name in middle -->
<div class="playlist-title-badge outfit-font" *ngIf="playlistService.activeListName() && !playlistService.selectionMode()">
<span class="playlist-title-text">{{ playlistService.activeListName() }}</span>
</div>
<!-- Active Playlist Actions -->
<div class="selection-pill glass" *ngIf="playlistService.activeListName() !== null && !playlistService.selectionMode()">
<ion-button fill="clear" color="secondary" (click)="refreshActiveRemotePlaylist()" class="mini-action-btn" title="Aggiorna Playlist" *ngIf="isRemotePlaylist()">
<ion-icon slot="icon-only" name="refresh-outline" [style.color]="playlistService.hasRemotePlaylistUpdate() ? 'var(--ion-color-warning)' : 'inherit'"></ion-icon>
</ion-button>
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn" *ngIf="!isComunitaPlaylist() && isActivePlaylistSaved()">
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="secondary" (click)="cloneActivePlaylist()" class="mini-action-btn" title="Clona Playlist">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="secondary" (click)="printActivePlaylist()" class="mini-action-btn" title="Esporta PDF">
<ion-icon slot="icon-only" name="print-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="secondary" (click)="shareActivePlaylist()" class="mini-action-btn">
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="danger" (click)="deleteActivePlaylist()" class="mini-action-btn" *ngIf="playlistService.activeListName() !== null && !isComunitaPlaylist()">
<ion-icon name="trash-outline" slot="icon-only"></ion-icon>
</ion-button>
</div>
<!-- Selection Mode Actions -->
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
<span class="selection-count">{{ reorderList().length }}</span>
<input
type="text"
[value]="playlistService.activeListName() || ''"
(input)="playlistService.activeListName.set($any($event.target).value)"
placeholder="Nome playlist..."
class="playlist-name-input outfit-font"
/>
<ion-button fill="clear" color="secondary" (click)="toggleAddingSongs()" class="mini-action-btn" *ngIf="reorderList().length > 0">
<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]="reorderList().length === 0" class="mini-action-btn" *ngIf="reorderList().length > 0">
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="danger" (click)="cancelSelection()" class="mini-action-btn" *ngIf="reorderList().length > 0">
<ion-icon slot="icon-only" name="close-circle-outline"></ion-icon>
</ion-button>
</div>
</div>
</ion-toolbar>
</ion-header>
<ion-content class="bg-gradient"
(touchstart)="onTouchStart($event); onInteraction()"
(touchmove)="onTouchMove($event)"
(touchend)="onTouchEnd()"
(click)="onInteraction()">
<div class="ion-padding no-padding-top">
<div *ngIf="cantiService.loading() && filteredCanti().length === 0" class="ion-text-center ion-padding loading-container">
<div class="loading-wrapper">
<ion-spinner name="crescent" color="secondary"></ion-spinner>
<div class="percentage-label outfit-font">{{ cantiService.progress() }}%</div>
<ion-progress-bar [value]="cantiService.progress() / 100" color="secondary" style="width: 200px; border-radius: 10px; height: 8px; margin-top: 8px;"></ion-progress-bar>
<p class="ion-margin-top" style="color: var(--ion-color-secondary)">Caricamento canti...</p>
</div>
</div>
<!-- Wrap in DragDrop only in Reorder mode -->
<div cdkDropList
[cdkDropListDisabled]="!playlistService.selectionMode() || isAddingSongs()"
(cdkDropListDropped)="drop($event)"
class="transparent-list">
<!-- Premium Suggested Mass Card (Collapsible with Swipe support) -->
<div class="suggeriti-header-card glass ion-margin-bottom"
*ngIf="showSuggeriti() && cantiLettureService.data()"
(click)="isMassCardExpanded.set(!isMassCardExpanded())"
(touchstart)="onMassCardTouchStart($event)"
(touchend)="onMassCardTouchEnd($event)"
[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>
<div class="card-header-right" style="display: flex; align-items: center; gap: 8px;">
<div class="mass-nav-buttons" style="display: flex; align-items: center; gap: 4px; z-index: 10;" (click)="$event.stopPropagation()">
<ion-button fill="clear" size="small" color="secondary" (click)="navigateMassDate(-1)" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 28px;">
<ion-icon slot="icon-only" name="chevron-back-outline" style="font-size: 1.1rem;"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" color="secondary" (click)="navigateMassDate(1)" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 28px;">
<ion-icon slot="icon-only" name="chevron-forward-outline" style="font-size: 1.1rem;"></ion-icon>
</ion-button>
</div>
<ion-icon [name]="isMassCardExpanded() ? 'chevron-up-outline' : 'chevron-down-outline'" class="expand-icon" style="margin-left: 4px;"></ion-icon>
</div>
</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"
[class.is-playing]="youtubePlayerService.currentCantoId() === canto.id"
cdkDrag
[cdkDragDisabled]="!playlistService.selectionMode() || isAddingSongs()">
<!-- Drag Handle (visible only in reorder mode) -->
<div class="drag-handle" cdkDragHandle *ngIf="playlistService.selectionMode() && !isAddingSongs()">
<ion-icon name="reorder-two-outline"></ion-icon>
</div>
<div class="item-wrapper">
<!-- Selection Area -->
<div class="selection-column" (click)="toggleSongSelection(canto.id); $event.stopPropagation()">
<span class="canto-number" [class.selected-number]="playlistService.selectedIds().has(canto.id)">
{{ canto.id.startsWith('my_') ? getMySongNumber(canto) : canto.id_canti }}
</span>
</div>
<!-- Content Area -->
<div class="content-column" (click)="goToCanto(canto.id)">
<div class="top-row">
<ion-label class="info-section">
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary); display: flex; align-items: center; flex-wrap: wrap; gap: 4px;">
<span *ngIf="getCommunitySongNumber(canto)" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto) }}</span>
<span>{{ canto.titolo }}</span>
<span *ngIf="canto.nonValidato && !canto.id.startsWith('my_')" class="non-validato-badge remote-badge">Remoto</span>
<span *ngIf="canto.id.startsWith('my_')" class="non-validato-badge mio-badge">Mio</span>
</h2>
<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>
</p>
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px;"
*ngIf="(settingsService.showDurationBpmTonality() && (canto.durata || canto.bpm || getSongTonality(canto))) || (settingsService.showUpdateDate() && canto.data_update) || (showTopTen() && getEsecuzioniCount(canto.id) !== null) || (showSuggeriti() && getMassSuggestionWeight(canto.id_canti) !== null)">
<ng-container *ngIf="settingsService.showDurationBpmTonality()">
<span *ngIf="canto.durata" style="font-size: 0.7rem; color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); padding: 1px 5px; border-radius: 4px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15);">
<ion-icon name="time-outline" style="font-size: 0.75rem;"></ion-icon>
{{ canto.durata }}
</span>
<span *ngIf="canto.bpm" style="font-size: 0.7rem; color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); padding: 1px 5px; border-radius: 4px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15);">
<ion-icon name="pulse-outline" style="font-size: 0.75rem;"></ion-icon>
{{ canto.bpm }} BPM
</span>
<span *ngIf="getSongTonality(canto)" style="font-size: 0.7rem; color: var(--ion-color-secondary); opacity: 0.85; background: rgba(var(--ion-color-secondary-rgb), 0.08); padding: 1px 5px; border-radius: 4px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.15);">
<ion-icon name="musical-note-outline" style="font-size: 0.75rem;"></ion-icon>
{{ getSongTonality(canto) }}
</span>
</ng-container>
<span *ngIf="settingsService.showUpdateDate() && canto.data_update" class="update-date-badge" style="font-size: 0.7rem; color: rgba(255, 255, 255, 0.45); font-weight: 400; display: inline-flex; align-items: center; gap: 3px;">
<ion-icon name="calendar-outline" style="font-size: 0.75rem; color: rgba(255,255,255,0.45);"></ion-icon>
agg. {{ cantiService.formatUpdateDate(canto.data_update) }}
</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>
<!-- Visualizza tag sotto autore -->
<div *ngIf="settingsService.showTagsInList() && cantiService.getSongTags(canto).length > 0" class="song-tags-list">
<span *ngFor="let tag of cantiService.getSongTags(canto)" class="song-tag-badge">
{{ tag }}
</span>
</div>
</ion-label>
<!-- Video Thumbnail Section -->
<div *ngIf="cantiService.getYoutubeId(canto.link_youtube)" class="thumb-section" (click)="$event.stopPropagation()">
<div class="thumb-container compact">
<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"
loading="lazy">
<div class="play-overlay youtube-overlay">
<ion-icon name="play-sharp"></ion-icon>
</div>
</div>
</ng-container>
<ng-template #noPlayerThumb>
<div class="thumb-wrapper">
<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>
</div>
<!-- Delete Button (only for local songs) -->
<div *ngIf="canto.id.startsWith('my_')" class="delete-section" (click)="$event.stopPropagation()">
<ion-button fill="clear" color="danger" (click)="deleteMyCanto(canto.id, $event)">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
</ion-button>
</div>
</div>
</div>
<!-- Player bars removed from here -->
</div>
</ion-item>
</div>
<!-- Add songs / Toggle View Button -->
<div class="ion-text-center ion-margin-top" *ngIf="playlistService.selectionMode()">
<ion-button fill="solid" color="secondary" (click)="toggleAddingSongs()" class="action-mode-btn glass">
<ion-icon slot="start" [name]="isAddingSongs() ? 'list-outline' : 'add-circle-outline'"></ion-icon>
{{ isAddingSongs() ? 'Torna alla Playlist' : 'Aggiungi altri canti' }}
</ion-button>
</div>
<div *ngIf="!cantiService.loading() && filteredCanti().length === 0" class="ion-text-center ion-padding">
<p style="color: rgba(255,255,255,0.5)">Nessun canto trovato.</p>
</div>
</div>
<ion-infinite-scroll (ionInfinite)="loadData($event)" threshold="150px" [disabled]="(comunitaService.isFilterActive() && comunitaService.comunitaCode()) || limit() >= filteredCanti().length || (playlistService.selectionMode() && !isAddingSongs())">
<ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Caricamento altri canti...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
<ion-footer *ngIf="youtubePlayerService.currentCantoId()" class="ion-no-border">
<ion-toolbar class="global-player-toolbar glass">
<div class="player-content">
<div class="controls-row">
<ion-button fill="clear" color="secondary" (click)="playPrevPreview($event)" class="skip-btn">
<ion-icon slot="icon-only" name="chevron-back"></ion-icon>
</ion-button>
<ng-container *ngIf="youtubePlayerService.isPlayerSupported() && getPlayingCanto()?.link_youtube && getPlayingCanto()?.link_youtube!.length > 5; else noAudioMsg">
<ion-button fill="clear" color="secondary" (click)="togglePlayPause()" class="play-btn">
<ion-icon slot="icon-only" [name]="youtubePlayerService.isPlaying() ? 'pause-sharp' : 'play-sharp'"></ion-icon>
</ion-button>
<span class="player-time-label outfit-font" style="font-size: 0.85rem; color: var(--ion-color-secondary); opacity: 0.8; min-width: 38px; text-align: right; margin-left: 4px;">{{ formatSeconds(youtubePlayerService.videoProgress()) }}</span>
<ion-range
[min]="0"
[max]="youtubePlayerService.videoDuration()"
[value]="youtubePlayerService.videoProgress()"
(ionChange)="onSeek($event)"
(ionKnobMoveStart)="onSeekStart()"
(ionKnobMoveEnd)="onSeekEnd()"
color="secondary"
class="global-range"
style="margin: 0; padding: 0 4px;">
</ion-range>
<span class="player-time-label outfit-font" style="font-size: 0.85rem; color: var(--ion-color-secondary); opacity: 0.8; min-width: 38px; text-align: left; margin-right: 4px;">{{ formatSeconds(youtubePlayerService.videoDuration()) }}</span>
</ng-container>
<ng-template #noAudioMsg>
<div class="no-audio-msg outfit-font" style="flex: 1; text-align: center; font-size: 0.85rem; opacity: 0.6; color: var(--ion-color-medium);">
Audio non disponibile
</div>
</ng-template>
<ion-button fill="clear" color="secondary" (click)="playNextPreview($event)" class="skip-btn">
<ion-icon slot="icon-only" name="chevron-forward"></ion-icon>
</ion-button>
</div>
</div>
</ion-toolbar>
</ion-footer>
<div class="black-screen-overlay" *ngIf="isBlackOverlayActive()" (click)="isBlackOverlayDismissed.set(true)">
<div class="black-screen-content" (click)="$event.stopPropagation()">
<div (click)="isBlackOverlayDismissed.set(true)" style="display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; width: 100%;">
<p class="car-mode-text outfit-font">Schermo nero attivo</p>
</div>
<div class="car-mode-canto-info" *ngIf="getPlayingCanto()">
<h2 class="car-mode-canto-title outfit-font">{{ getPlayingCanto()?.titolo }}</h2>
<p class="car-mode-canto-author outfit-font">{{ getPlayingCanto()?.autore }}</p>
</div>
<div class="car-mode-progress outfit-font" *ngIf="youtubePlayerService.currentCantoId()">
<span class="car-mode-time">{{ formatSeconds(youtubePlayerService.videoProgress()) }} / {{ formatSeconds(youtubePlayerService.videoDuration()) }}</span>
</div>
<div class="car-mode-controls">
<ion-button fill="clear" (click)="playPrevPreview($event)" class="car-mode-btn">
<ion-icon slot="icon-only" name="play-skip-back-sharp"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="youtubePlayerService.isPlaying() ? pauseVideo($event) : playVideo($event, youtubePlayerService.currentCantoId()!)" class="car-mode-btn">
<ion-icon slot="icon-only" [name]="youtubePlayerService.isPlaying() ? 'pause-sharp' : 'play-sharp'"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="playNextPreview($event)" class="car-mode-btn">
<ion-icon slot="icon-only" name="play-skip-forward-sharp"></ion-icon>
</ion-button>
</div>
<p class="car-mode-subtext outfit-font" (click)="isBlackOverlayDismissed.set(true)">Tocca lo sfondo o l'icona per sbloccare</p>
</div>
</div>