fix: risoluzione importazione playlist tramite link di condivisione e fix installabilità PWA su Android
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<div class="header-logo-wrapper">
|
||||
<img src="assets/icon/favicon.png" class="header-logo">
|
||||
<div class="header-text-group">
|
||||
<span class="app-name">CantiCristiani</span>
|
||||
<span class="app-name">{{ appName }}</span>
|
||||
<span class="version-badge">v{{ version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,6 +192,29 @@
|
||||
(click)="onInteraction()">
|
||||
|
||||
<div class="ion-padding no-padding-top">
|
||||
<!-- Premium PWA Android Install Banner -->
|
||||
<div class="pwa-android-banner glass ion-margin-bottom" *ngIf="showAndroidBanner()">
|
||||
<div class="banner-inner">
|
||||
<div class="banner-icon-wrapper">
|
||||
<ion-icon name="cloud-download-outline" class="banner-icon"></ion-icon>
|
||||
</div>
|
||||
<div class="banner-text-wrapper">
|
||||
<h3 class="banner-title outfit-font">Installa la nostra App!</h3>
|
||||
<p class="banner-subtitle outfit-font">
|
||||
Accedi a tutti i canti all'istante, anche offline, direttamente dalla tua home screen.
|
||||
</p>
|
||||
</div>
|
||||
<div class="banner-actions">
|
||||
<ion-button fill="solid" size="small" color="secondary" class="install-action-btn outfit-font" (click)="installAndroidPwa()">
|
||||
Installa
|
||||
</ion-button>
|
||||
<ion-button fill="clear" size="small" class="dismiss-action-btn" (click)="dismissAndroidBanner($event)">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -394,3 +417,21 @@
|
||||
</div>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
<!-- Premium PWA iOS Tooltip -->
|
||||
<div class="pwa-ios-tooltip-wrapper" [class.has-player]="youtubePlayerService.currentCantoId()" *ngIf="showIosTooltip()">
|
||||
<div class="pwa-ios-tooltip glass">
|
||||
<div class="tooltip-header">
|
||||
<span class="tooltip-title outfit-font">Installa su iPhone</span>
|
||||
<ion-button fill="clear" size="small" class="dismiss-btn" (click)="dismissIosTooltip($event)">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<div class="tooltip-body">
|
||||
<p class="tooltip-text outfit-font">
|
||||
Tocca il pulsante Condividi <span class="safari-icon-inline"><ion-icon name="share-outline"></ion-icon></span> in basso e seleziona <strong>"Aggiungi a schermata Home"</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="tooltip-arrow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -953,3 +953,323 @@ ion-title {
|
||||
color: #c0392b !important;
|
||||
border-color: #c0392b !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
PWA INSTALLATION PROMOTIONS (Android & iOS)
|
||||
========================================================================== */
|
||||
|
||||
/* 1. Android Installation Banner */
|
||||
.pwa-android-banner {
|
||||
--background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-left: 4px solid var(--ion-color-secondary);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: slideInBanner 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.banner-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.banner-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(var(--ion-color-secondary-rgb), 0.12);
|
||||
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.2);
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.banner-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--ion-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.banner-text-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.banner-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.banner-subtitle {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.install-action-btn {
|
||||
--border-radius: 10px;
|
||||
--box-shadow: 0 4px 10px rgba(var(--ion-color-secondary-rgb), 0.2);
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.2px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.dismiss-action-btn {
|
||||
--color: rgba(255, 255, 255, 0.5);
|
||||
--padding-start: 4px;
|
||||
--padding-end: 4px;
|
||||
margin: 0;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. iOS Safari Contextual Tooltip */
|
||||
.pwa-ios-tooltip-wrapper {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10000;
|
||||
width: 90%;
|
||||
max-width: 385px;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 12px 36px rgba(0, 0, 0, 0.45));
|
||||
transition: bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.has-player {
|
||||
bottom: 84px; /* Float above active bottom player footer */
|
||||
}
|
||||
|
||||
.pwa-ios-tooltip {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(26, 26, 46, 0.96) !important;
|
||||
backdrop-filter: blur(25px);
|
||||
-webkit-backdrop-filter: blur(25px);
|
||||
border: 1.5px solid rgba(var(--ion-color-secondary-rgb), 0.25);
|
||||
border-radius: 20px;
|
||||
padding: 14px 16px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.02);
|
||||
animation: floatTooltip 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.275), bounceGently 3s ease-in-out infinite;
|
||||
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.tooltip-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--ion-color-secondary);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.dismiss-btn {
|
||||
--color: rgba(255, 255, 255, 0.45);
|
||||
--padding-start: 4px;
|
||||
--padding-end: 4px;
|
||||
margin: 0;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-body {
|
||||
.tooltip-text {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
|
||||
strong {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.safari-icon-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
vertical-align: middle;
|
||||
margin: 0 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 0.95rem;
|
||||
color: #007aff; /* Apple System Blue */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 12px solid transparent;
|
||||
border-right: 12px solid transparent;
|
||||
border-top: 12px solid rgba(26, 26, 46, 0.96);
|
||||
position: absolute;
|
||||
bottom: -11px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 12px solid transparent;
|
||||
border-right: 12px solid transparent;
|
||||
border-top: 12px solid rgba(var(--ion-color-secondary-rgb), 0.25);
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: -12px;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideInBanner {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatTooltip {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceGently {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast mode adaptations */
|
||||
:host-context(body.high-contrast) {
|
||||
.pwa-android-banner {
|
||||
background: #ffffff !important;
|
||||
border: 2px solid #000000 !important;
|
||||
border-left-width: 6px !important;
|
||||
|
||||
.banner-title, .banner-subtitle {
|
||||
color: #000000 !important;
|
||||
}
|
||||
.banner-icon {
|
||||
color: #000000 !important;
|
||||
}
|
||||
.banner-icon-wrapper {
|
||||
background: rgba(0, 0, 0, 0.05) !important;
|
||||
border-color: #000000 !important;
|
||||
}
|
||||
.install-action-btn {
|
||||
--background: #000000 !important;
|
||||
--color: #ffffff !important;
|
||||
}
|
||||
.dismiss-action-btn {
|
||||
--color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pwa-ios-tooltip {
|
||||
background: #ffffff !important;
|
||||
border: 2px solid #000000 !important;
|
||||
|
||||
.tooltip-title, .tooltip-text, .tooltip-text strong {
|
||||
color: #000000 !important;
|
||||
}
|
||||
.dismiss-btn {
|
||||
--color: #000000 !important;
|
||||
}
|
||||
.tooltip-arrow {
|
||||
border-top-color: #ffffff !important;
|
||||
&::after {
|
||||
border-top-color: #000000 !important;
|
||||
}
|
||||
}
|
||||
.safari-icon-inline {
|
||||
background: rgba(0, 0, 0, 0.05) !important;
|
||||
border-color: #000000 !important;
|
||||
ion-icon {
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+154
-77
@@ -15,6 +15,7 @@ import { MyCantiService } from '../services/my-canti.service';
|
||||
import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component';
|
||||
import { CantiLettureService } from '../services/canti-letture.service';
|
||||
import { ComunitaService } from '../services/comunita.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -33,10 +34,14 @@ export class HomePage implements OnDestroy {
|
||||
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null);
|
||||
public loadedThumbs = new Set<string>();
|
||||
public version = VERSION;
|
||||
public appName = environment.appName;
|
||||
public isAddingSongs = signal<boolean>(false);
|
||||
public reorderList = signal<any[]>([]);
|
||||
public limit = signal<number>(30);
|
||||
|
||||
public showAndroidBanner = signal<boolean>(false);
|
||||
public showIosTooltip = signal<boolean>(false);
|
||||
|
||||
public fontSize = signal<number>(1.0);
|
||||
public youtubePlayerService = inject(YoutubePlayerService);
|
||||
public comunitaService = inject(ComunitaService);
|
||||
@@ -110,23 +115,28 @@ export class HomePage implements OnDestroy {
|
||||
const onlyMine = this.showOnlyMine();
|
||||
const topTen = this.showTopTen();
|
||||
const suggeriti = this.showSuggeriti();
|
||||
|
||||
let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
||||
const activeIds = this.playlistService.activeListIds();
|
||||
const selectionMode = this.playlistService.selectionMode();
|
||||
|
||||
// Filter by Community if code is set and filter is active
|
||||
// 1. Get base list depending on community filter
|
||||
let list: any[] = [];
|
||||
const comunitaCode = this.comunitaService.comunitaCode();
|
||||
const comunitaIds = this.comunitaService.comunitaCantiIds();
|
||||
|
||||
if (comunitaCode && this.comunitaService.isFilterActive()) {
|
||||
// Include parish custom/unvalidated canti
|
||||
list = [...list, ...this.comunitaService.comunitaCantiPersonali()];
|
||||
|
||||
// Base is standard canti + my canti + community custom canti
|
||||
list = [
|
||||
...this.cantiService.canti(),
|
||||
...this.myCantiService.myCanti(),
|
||||
...this.comunitaService.comunitaCantiPersonali()
|
||||
];
|
||||
|
||||
// Filter to only community canti
|
||||
list = list.filter(c => {
|
||||
return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id);
|
||||
});
|
||||
|
||||
// Deduplicate: if a song exists in both validated and unvalidated lists,
|
||||
// the NON-VALIDATED (parish-customized) version always takes precedence,
|
||||
// so parishes can always personalize their songs even if never validated.
|
||||
// Deduplicate: parish-customized versions take precedence
|
||||
const seen = new Map<number | string, any>();
|
||||
for (const canto of list) {
|
||||
const key = canto.id_canti || canto.id;
|
||||
@@ -134,16 +144,14 @@ export class HomePage implements OnDestroy {
|
||||
if (!existing) {
|
||||
seen.set(key, canto);
|
||||
} else {
|
||||
// Precedence to non-validated (parish custom) version:
|
||||
// overwrite only if current entry is validated and new one is non-validated
|
||||
if (!existing.nonValidato && canto.nonValidato) {
|
||||
seen.set(key, canto);
|
||||
}
|
||||
}
|
||||
}
|
||||
list = Array.from(seen.values());
|
||||
|
||||
// Sort list by community song number (num_canto) ASC!
|
||||
|
||||
// Sort by community song number (num_canto)
|
||||
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||
list.sort((a, b) => {
|
||||
const infoA = cantiInfo.find(x => x.id_canti === a.id_canti || x.id_canti === Number(a.id));
|
||||
@@ -152,49 +160,77 @@ export class HomePage implements OnDestroy {
|
||||
const numB = infoB ? Number(infoB.num_canto) : 999999;
|
||||
return numA - numB;
|
||||
});
|
||||
} else {
|
||||
// General context: only standard canti + my canti
|
||||
list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
||||
}
|
||||
|
||||
if (onlyMine) {
|
||||
list = this.myCantiService.myCanti();
|
||||
// 2. Cumulative filtering
|
||||
|
||||
// Filter by selectionMode / reorderList if in selection mode and not adding songs
|
||||
if (selectionMode && !this.isAddingSongs()) {
|
||||
return this.reorderList();
|
||||
}
|
||||
|
||||
|
||||
// A. Filter by Playlist (if activeIds is present)
|
||||
if (activeIds.length > 0 && !selectionMode) {
|
||||
list = activeIds
|
||||
.map(id => list.find(c => c.id === id))
|
||||
.filter((c): c is any => !!c);
|
||||
}
|
||||
|
||||
// B. Filter by "Only Mine" (Miei)
|
||||
if (onlyMine) {
|
||||
const myIds = new Set(this.myCantiService.myCanti().map(c => c.id));
|
||||
list = list.filter(c => myIds.has(c.id));
|
||||
}
|
||||
|
||||
// C. Filter by Liturgical Moment
|
||||
if (litId !== null) {
|
||||
list = list.filter(c => c.id_momenti?.includes(litId));
|
||||
}
|
||||
|
||||
// D. Filter by Thematic Moment
|
||||
if (temId !== null) {
|
||||
list = list.filter(c => c.id_momenti?.includes(temId));
|
||||
}
|
||||
|
||||
// E. Filter by Suggested (Suggeriti)
|
||||
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
|
||||
if (selectionMode && !this.isAddingSongs()) {
|
||||
return this.reorderList();
|
||||
// F. Filter by Search Query
|
||||
if (query) {
|
||||
const processedQuery = query.replace(/\buno\b/g, '1');
|
||||
const numberMatchPattern = processedQuery.match(/^(?:numero|nr\.?)\s*(\d+)$/);
|
||||
|
||||
if (numberMatchPattern) {
|
||||
const targetId = numberMatchPattern[1];
|
||||
list = list.filter(c => {
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
|
||||
});
|
||||
} else {
|
||||
list = list.filter(c => {
|
||||
const titleMatch = this.normalize(c.titolo).includes(query);
|
||||
const authorMatch = this.normalize(c.autore).includes(query);
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query);
|
||||
|
||||
const lyricsPlain = (c.testo || '')
|
||||
.replace(/{.*?}/g, '')
|
||||
.replace(/\n/g, ' ');
|
||||
const lyricsMatch = this.normalize(lyricsPlain).includes(query);
|
||||
|
||||
return titleMatch || authorMatch || lyricsMatch || numberMatch;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an active playlist and NOT in selection mode, show ONLY those songs
|
||||
if (activeIds.length > 0 && !selectionMode) {
|
||||
// Filter only songs in the special list and KEEP THE ORDER
|
||||
return activeIds
|
||||
.map(id => list.find(c => c.id === id))
|
||||
.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) {
|
||||
// G. Sorting/Ordering
|
||||
if (topTen) {
|
||||
const eseguiti = this.cantiService.cantiEseguiti();
|
||||
const eseguitiMap = new Map<number, number>();
|
||||
eseguiti.forEach(x => eseguitiMap.set(x.id_canti, x.num));
|
||||
@@ -204,37 +240,16 @@ export class HomePage implements OnDestroy {
|
||||
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")
|
||||
const processedQuery = query.replace(/\buno\b/g, '1');
|
||||
|
||||
// Se la ricerca inizia con "numero" o "nr", filtra esattamente per id_canti/comunita number
|
||||
const numberMatchPattern = processedQuery.match(/^(?:numero|nr\.?)\s*(\d+)$/);
|
||||
if (numberMatchPattern) {
|
||||
const targetId = numberMatchPattern[1];
|
||||
return list.filter(c => {
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
|
||||
} else 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;
|
||||
});
|
||||
}
|
||||
|
||||
return list.filter(c => {
|
||||
const titleMatch = this.normalize(c.titolo).includes(query);
|
||||
const authorMatch = this.normalize(c.autore).includes(query);
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query);
|
||||
|
||||
// Strip tags like {Chorus} and newlines from lyrics before searching
|
||||
const lyricsPlain = (c.testo || '')
|
||||
.replace(/{.*?}/g, '')
|
||||
.replace(/\n/g, ' ');
|
||||
const lyricsMatch = this.normalize(lyricsPlain).includes(query);
|
||||
|
||||
return titleMatch || authorMatch || lyricsMatch || numberMatch;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
public visibleCanti = computed(() => {
|
||||
@@ -242,6 +257,49 @@ export class HomePage implements OnDestroy {
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Check if install prompts should be visible
|
||||
const androidDismissed = localStorage.getItem('pwa-android-dismissed') === 'true';
|
||||
const iosDismissed = localStorage.getItem('pwa-ios-dismissed') === 'true';
|
||||
|
||||
this.showAndroidBanner.set(
|
||||
this.settingsService.isAndroid() &&
|
||||
!this.settingsService.isStandalone() &&
|
||||
!androidDismissed
|
||||
);
|
||||
|
||||
this.showIosTooltip.set(
|
||||
this.settingsService.isIos() &&
|
||||
!this.settingsService.isStandalone() &&
|
||||
!iosDismissed
|
||||
);
|
||||
|
||||
// Track initial community filter state to avoid clearing during the initial run of the effect
|
||||
let prevFilterActive = this.comunitaService.isFilterActive();
|
||||
let prevComunitaCode = this.comunitaService.comunitaCode();
|
||||
|
||||
// Automatically reset all other filters and active playlist when toggling the community filter or changing community
|
||||
effect(() => {
|
||||
const active = this.comunitaService.isFilterActive();
|
||||
const code = this.comunitaService.comunitaCode();
|
||||
|
||||
if (active !== prevFilterActive || code !== prevComunitaCode) {
|
||||
prevFilterActive = active;
|
||||
prevComunitaCode = code;
|
||||
|
||||
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);
|
||||
this.searchQuery.set('');
|
||||
this.activeFilterType.set(null);
|
||||
this.limit.set(10);
|
||||
}
|
||||
}, { allowSignalWrites: true });
|
||||
|
||||
// Sync speech recognition results to search query
|
||||
effect(() => {
|
||||
const transcript = this.audioEngine.searchTranscript();
|
||||
@@ -307,6 +365,34 @@ export class HomePage implements OnDestroy {
|
||||
}, { allowSignalWrites: true });
|
||||
}
|
||||
|
||||
dismissAndroidBanner(event?: Event) {
|
||||
if (event) event.stopPropagation();
|
||||
this.showAndroidBanner.set(false);
|
||||
localStorage.setItem('pwa-android-dismissed', 'true');
|
||||
}
|
||||
|
||||
dismissIosTooltip(event?: Event) {
|
||||
if (event) event.stopPropagation();
|
||||
this.showIosTooltip.set(false);
|
||||
localStorage.setItem('pwa-ios-dismissed', 'true');
|
||||
}
|
||||
|
||||
async installAndroidPwa() {
|
||||
if (this.settingsService.deferredPrompt()) {
|
||||
await this.settingsService.installPwa();
|
||||
this.dismissAndroidBanner();
|
||||
} else {
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Tocca il menu del browser (i tre puntini in alto a destra) e seleziona "Aggiungi a schermata Home" o "Installa app".',
|
||||
duration: 6000,
|
||||
position: 'bottom',
|
||||
color: 'secondary',
|
||||
buttons: [{ text: 'OK', role: 'cancel' }]
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
}
|
||||
|
||||
handleImport(base64: string) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(escape(atob(base64)));
|
||||
@@ -382,10 +468,6 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
toggleOnlyMine() {
|
||||
this.showOnlyMine.update(v => !v);
|
||||
if (this.showOnlyMine()) {
|
||||
this.selectedLiturgico.set(null);
|
||||
this.selectedTematico.set(null);
|
||||
}
|
||||
this.limit.set(10);
|
||||
}
|
||||
|
||||
@@ -401,7 +483,6 @@ export class HomePage implements OnDestroy {
|
||||
} else if (type === 'tematico') {
|
||||
this.selectedTematico.update(cur => cur === id ? null : id);
|
||||
}
|
||||
this.showOnlyMine.set(false);
|
||||
this.activeFilterType.set(null);
|
||||
this.limit.set(10);
|
||||
}
|
||||
@@ -510,10 +591,6 @@ export class HomePage implements OnDestroy {
|
||||
this.playlistService.activePlaylistId.set(pl.id);
|
||||
this.activeFilterType.set(null);
|
||||
this.limit.set(50);
|
||||
// Clear other filters
|
||||
this.selectedLiturgico.set(null);
|
||||
this.selectedTematico.set(null);
|
||||
this.showOnlyMine.set(false);
|
||||
}
|
||||
|
||||
toggleTopTen() {
|
||||
@@ -580,7 +657,7 @@ export class HomePage implements OnDestroy {
|
||||
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;
|
||||
return es ? es.num : 0;
|
||||
}
|
||||
|
||||
onImgLoad(id: string) {
|
||||
|
||||
Reference in New Issue
Block a user