feat: show instructions when playlist is empty & fix high contrast contrast

This commit is contained in:
David Frassi
2026-06-15 00:24:27 +02:00
parent b4b7e10c5f
commit 1b6e8a1832
12 changed files with 554 additions and 370 deletions
+18 -20
View File
@@ -6,7 +6,7 @@
<div class="header-text-group">
<span class="app-name">{{ appName }}</span>
<span class="version-badge" (click)="checkForAppUpdate($event)" style="cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">
v{{ version }}
v{{ version }}<span *ngIf="settingsService.userName()"> - {{ settingsService.userName() }}</span>
<span *ngIf="hasUpdateAvailable()" style="display: inline-flex; align-items: center; justify-content: center; padding: 4px; margin-left: 2px; background: rgba(var(--ion-color-secondary-rgb), 0.25); border-radius: 50%; border: 1px solid var(--ion-color-secondary); width: 24px; height: 24px; box-shadow: 0 0 8px rgba(var(--ion-color-secondary-rgb), 0.3);">
<ion-icon name="refresh-outline" style="font-size: 1.15rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
</span>
@@ -48,11 +48,10 @@
<ion-button
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
size="small"
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
(click)="toggleFilterType('playlist')"
class="filter-chip">
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length > 0"></ion-icon>
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length === 0"></ion-icon>
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null"></ion-icon>
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
@@ -69,14 +68,6 @@
<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
*ngIf="comunitaService.comunitaCode()"
(click)="editComunitaCode($event)"
style="position: absolute; top: -6px; right: -6px; z-index: 99999; background: var(--ion-color-secondary); color: white; border-radius: 50%; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; border: 1.5px solid #1a1a1a; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"
class="floating-edit-badge">
<ion-icon name="create-outline" style="font-size: 0.85rem; pointer-events: none;"></ion-icon>
</div>
</div>
<ion-button
@@ -143,18 +134,18 @@
<!-- Category Scroll (Dropdown style) -->
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="activeFilterType() || playlistService.selectionMode()">
<div class="momento-scroll">
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="shouldShowSubSectionToolbar()">
<div class="momento-scroll" *ngIf="activeFilterType() !== 'playlist' || playlistService.allPlaylists().length > 0 || playlistService.selectionMode()">
<!-- Selection Mode Actions -->
<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">
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
<span class="selection-count">{{ reorderList().length }}</span>
<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]="playlistService.selectedIds().size === 0" class="mini-action-btn">
<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">
<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>
@@ -191,7 +182,7 @@
name="add-outline"
(click)="$event.stopPropagation()"
routerLink="/propose-canto"
style="font-size: 1.1rem; color: #000; font-weight: bold; background: rgba(0,0,0,0.1); border-radius: 50%; padding: 2px;">
class="non-validati-add-icon">
</ion-icon>
</div>
</ng-container>
@@ -208,6 +199,13 @@
</div>
</ng-container>
</div>
<!-- Instructions when playlist is empty -->
<div class="empty-playlist-container" *ngIf="activeFilterType() === 'playlist' && playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()">
<p class="empty-playlist-instruction outfit-font">
Per creare una playlist, seleziona il nr dei canti che vuoi inserire nella playlist, riordinali e salvala con nome
</p>
</div>
</ion-toolbar>
</ion-header>
+53
View File
@@ -368,6 +368,24 @@ ion-title {
}
}
.non-validati-add-icon {
font-size: 1.1rem;
color: #000;
font-weight: bold;
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
padding: 2px;
display: inline-block;
vertical-align: middle;
}
:host-context(body.high-contrast) {
.non-validati-add-icon {
color: #ffffff !important;
background: rgba(255, 255, 255, 0.2) !important;
}
}
// Custom Item Layout
.custom-item {
--padding-start: 16px;
@@ -1285,3 +1303,38 @@ ion-title {
}
}
}
.empty-playlist-container {
padding: 12px 16px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
box-sizing: border-box;
}
.empty-playlist-instruction {
margin: 0;
font-size: 0.85rem;
line-height: 1.4;
color: rgba(255, 255, 255, 0.8);
text-align: center;
background: rgba(var(--ion-color-secondary-rgb), 0.08);
border: 1px dashed rgba(var(--ion-color-secondary-rgb), 0.35);
padding: 12px 16px;
border-radius: 12px;
max-width: 480px;
width: 100%;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
:host-context(body.high-contrast) {
.empty-playlist-instruction {
color: #000000 !important;
background: rgba(var(--ion-color-secondary-rgb), 0.12) !important;
border-color: var(--ion-color-secondary) !important;
box-shadow: none !important;
}
}
+19 -95
View File
@@ -773,9 +773,15 @@ export class HomePage implements OnDestroy {
if (response.ok) {
const remoteJson = await response.json();
if (Array.isArray(remoteJson)) {
// Reconstruct user name/metadata
const userMetadata = remoteJson.find((item: any) => item.momenti && item.momenti.includes('UserMetadata'));
if (userMetadata && userMetadata.titolo) {
this.settingsService.setUserName(userMetadata.titolo);
}
// Reconstruct custom songs
const customSongs = remoteJson
.filter((item: any) => !item.momenti || !item.momenti.includes('Playlist'))
.filter((item: any) => (!item.momenti || !item.momenti.includes('Playlist')) && (!item.momenti || !item.momenti.includes('UserMetadata')))
.map((item: any) => ({
id: `my_${item.id_canti}`,
id_canti: Number(item.id_canti),
@@ -1025,6 +1031,17 @@ export class HomePage implements OnDestroy {
this.searchQuery() !== '';
}
shouldShowSubSectionToolbar(): boolean {
if (this.playlistService.selectionMode() && this.reorderList().length > 0) {
return true;
}
const filterType = this.activeFilterType();
if (filterType) {
return true;
}
return false;
}
clearAllFilters() {
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
@@ -1655,105 +1672,12 @@ export class HomePage implements OnDestroy {
}
toggleComunitaFilter() {
if (!this.comunitaService.comunitaCode()) {
this.promptComunitaCode();
} else {
if (this.comunitaService.comunitaCode()) {
this.comunitaService.isFilterActive.update(v => !v);
}
this.activeFilterType.set(null);
}
editComunitaCode(event: Event) {
event.stopPropagation();
this.promptComunitaCode();
}
async promptComunitaCode() {
const alert = await this.alertCtrl.create({
header: 'Imposta Comunità',
subHeader: 'Inserisci il codice parrocchiale/comunità per attivare il libretto dedicato:',
cssClass: 'premium-alert',
inputs: [
{
name: 'code',
type: 'text',
placeholder: 'Es: 123456',
value: this.comunitaService.comunitaCode()
}
],
buttons: [
{
text: 'Annulla',
role: 'cancel'
},
{
text: 'Rimuovi',
role: 'destructive',
cssClass: 'alert-button-delete',
handler: async () => {
await this.comunitaService.setComunitaCode('');
const toast = await this.toastCtrl.create({
message: 'Comunità disattivata.',
duration: 2000,
color: 'secondary'
});
await toast.present();
}
},
{
text: 'Salva',
handler: async (data) => {
const trimmed = (data.code || '').trim();
if (!trimmed) {
await this.comunitaService.setComunitaCode('');
return;
}
// Show loading overlay with progress
const loading = await this.loadingCtrl.create({
message: 'Caricamento 0%',
cssClass: 'premium-loading',
spinner: 'crescent'
});
await loading.present();
// Subscribe to progress updates
let progressInterval: any = null;
progressInterval = setInterval(() => {
const pct = this.comunitaService.loadingProgress();
loading.message = `Caricamento ${pct}%`;
if (pct >= 100) {
clearInterval(progressInterval);
}
}, 100);
const success = await this.comunitaService.setComunitaCode(trimmed);
clearInterval(progressInterval);
await loading.dismiss();
if (success) {
const toast = await this.toastCtrl.create({
message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`,
duration: 2000,
color: 'success'
});
await toast.present();
} else {
const toast = await this.toastCtrl.create({
message: 'Codice non trovato o errore di connessione.',
duration: 2000,
color: 'danger'
});
await toast.present();
setTimeout(() => this.promptComunitaCode(), 500);
}
}
}
]
});
await alert.present();
}
private massCardStartX: number = 0;
private massCardStartY: number = 0;