gestione comunità
This commit is contained in:
@@ -43,6 +43,27 @@
|
||||
{{ filteredCanti().length }}
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<div class="comunita-filter-wrapper" style="position: relative; display: inline-flex; align-items: center; margin-right: 8px; 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
|
||||
*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
|
||||
*ngIf="settingsService.showEditor()"
|
||||
[fill]="showOnlyMine() ? 'solid' : 'outline'"
|
||||
@@ -183,10 +204,12 @@
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
class="transparent-list">
|
||||
|
||||
<!-- Premium Suggested Mass Card (Collapsible) -->
|
||||
<!-- 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">
|
||||
@@ -194,7 +217,17 @@
|
||||
<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 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()">
|
||||
@@ -234,7 +267,7 @@
|
||||
<div class="top-row">
|
||||
<ion-label class="info-section">
|
||||
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary)">
|
||||
{{ canto.titolo }}
|
||||
<span *ngIf="getCommunitySongNumber(canto)" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto) }}</span>{{ canto.titolo }}
|
||||
</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>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
padding: 8px 4px 8px 0;
|
||||
|
||||
// Hide scrollbar but keep functionality
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
+229
-9
@@ -9,11 +9,12 @@ import { PlaylistService } from '../services/playlist.service';
|
||||
import { SettingsService } from '../services/settings.service';
|
||||
import { VERSION } from '../version';
|
||||
import { YoutubePlayerService } from '../services/youtube-player.service';
|
||||
import { ModalController, AlertController, ToastController } from '@ionic/angular';
|
||||
import { ModalController, AlertController, ToastController, LoadingController } from '@ionic/angular';
|
||||
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';
|
||||
import { ComunitaService } from '../services/comunita.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -38,6 +39,7 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
public fontSize = signal<number>(1.0);
|
||||
public youtubePlayerService = inject(YoutubePlayerService);
|
||||
public comunitaService = inject(ComunitaService);
|
||||
public isSeeking = false;
|
||||
|
||||
private readonly MIN_FONT = 0.6;
|
||||
@@ -59,6 +61,7 @@ export class HomePage implements OnDestroy {
|
||||
private modalCtrl = inject(ModalController);
|
||||
private alertCtrl = inject(AlertController);
|
||||
private toastCtrl = inject(ToastController);
|
||||
private loadingCtrl = inject(LoadingController);
|
||||
|
||||
private firstInteraction = true;
|
||||
|
||||
@@ -70,7 +73,14 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
private scrollEffect = effect(() => {
|
||||
const activeId = this.youtubePlayerService.currentCantoId();
|
||||
const list = this.filteredCanti();
|
||||
|
||||
if (activeId) {
|
||||
const index = list.findIndex(c => c.id === activeId);
|
||||
if (index !== -1 && index >= this.limit()) {
|
||||
this.limit.set(index + 15);
|
||||
}
|
||||
|
||||
// Delay slightly to ensure DOM is updated and classes are applied
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById('canto-' + activeId);
|
||||
@@ -79,7 +89,7 @@ export class HomePage implements OnDestroy {
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}, { allowSignalWrites: true });
|
||||
|
||||
private normalize(str: string | undefined): string {
|
||||
if (!str) return '';
|
||||
@@ -103,6 +113,25 @@ export class HomePage implements OnDestroy {
|
||||
|
||||
let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
||||
|
||||
// Filter by Community if code is set and filter is active
|
||||
const comunitaCode = this.comunitaService.comunitaCode();
|
||||
const comunitaIds = this.comunitaService.comunitaCantiIds();
|
||||
if (comunitaCode && this.comunitaService.isFilterActive()) {
|
||||
list = list.filter(c => {
|
||||
return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id);
|
||||
});
|
||||
|
||||
// Sort list by community song number (num_canto) ASC!
|
||||
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));
|
||||
const infoB = cantiInfo.find(x => x.id_canti === b.id_canti || x.id_canti === Number(b.id));
|
||||
const numA = infoA ? Number(infoA.num_canto) : 999999;
|
||||
const numB = infoB ? Number(infoB.num_canto) : 999999;
|
||||
return numA - numB;
|
||||
});
|
||||
}
|
||||
|
||||
if (onlyMine) {
|
||||
list = this.myCantiService.myCanti();
|
||||
}
|
||||
@@ -160,17 +189,21 @@ export class HomePage implements OnDestroy {
|
||||
// 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
|
||||
// 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 => c.id_canti.toString() === targetId);
|
||||
return list.filter(c => {
|
||||
const commNum = this.getCommunitySongNumber(c);
|
||||
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
|
||||
});
|
||||
}
|
||||
|
||||
return list.filter(c => {
|
||||
const titleMatch = this.normalize(c.titolo).includes(query);
|
||||
const authorMatch = this.normalize(c.autore).includes(query);
|
||||
const numberMatch = c.id_canti.toString().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 || '')
|
||||
@@ -642,10 +675,6 @@ export class HomePage implements OnDestroy {
|
||||
setTimeout(() => {
|
||||
this.limit.update(l => l + 10);
|
||||
event.target.complete();
|
||||
|
||||
if (this.limit() >= this.filteredCanti().length) {
|
||||
event.target.disabled = true;
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -883,4 +912,195 @@ export class HomePage implements OnDestroy {
|
||||
this.limit.set(10);
|
||||
}
|
||||
|
||||
getCommunitySongNumber(canto: any): string | null {
|
||||
if (!this.comunitaService.comunitaCode() || !this.comunitaService.isFilterActive()) return null;
|
||||
const cantiInfo = this.comunitaService.comunitaCantiInfo();
|
||||
const info = cantiInfo.find(x => x.id_canti === canto.id_canti || x.id_canti === Number(canto.id));
|
||||
return info && info.num_canto ? info.num_canto.toString() : null;
|
||||
}
|
||||
|
||||
toggleComunitaFilter() {
|
||||
if (!this.comunitaService.comunitaCode()) {
|
||||
this.promptComunitaCode();
|
||||
} else {
|
||||
this.comunitaService.isFilterActive.update(v => !v);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
onMassCardTouchStart(event: TouchEvent) {
|
||||
if (event.touches.length === 1) {
|
||||
this.massCardStartX = event.touches[0].clientX;
|
||||
this.massCardStartY = event.touches[0].clientY;
|
||||
}
|
||||
}
|
||||
|
||||
onMassCardTouchEnd(event: TouchEvent) {
|
||||
if (event.changedTouches.length === 1) {
|
||||
const endX = event.changedTouches[0].clientX;
|
||||
const endY = event.changedTouches[0].clientY;
|
||||
const diffX = endX - this.massCardStartX;
|
||||
const diffY = endY - this.massCardStartY;
|
||||
|
||||
// Ensure it is a horizontal swipe (diffX is much larger than diffY)
|
||||
if (Math.abs(diffX) > 60 && Math.abs(diffY) < 40) {
|
||||
// Prevent triggering click expansion
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (diffX > 0) {
|
||||
// Swiped right -> go to PREVIOUS date
|
||||
this.navigateMassDate(-1);
|
||||
} else {
|
||||
// Swiped left -> go to NEXT date
|
||||
this.navigateMassDate(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async navigateMassDate(direction: number) {
|
||||
const list = this.cantiLettureService.availableMasses();
|
||||
if (list.length === 0) return;
|
||||
|
||||
const currentDate = this.cantiLettureService.selectedMassDate();
|
||||
if (!currentDate) {
|
||||
this.cantiLettureService.selectedMassDate.set(list[0].date);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = list.findIndex(m => m.date === currentDate);
|
||||
if (currentIndex === -1) {
|
||||
this.cantiLettureService.selectedMassDate.set(list[0].date);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = currentIndex + direction;
|
||||
if (nextIndex >= 0 && nextIndex < list.length) {
|
||||
// Set the next/prev date
|
||||
this.cantiLettureService.selectedMassDate.set(list[nextIndex].date);
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: `Messa: ${list[nextIndex].day} ${this.formatCompactDate(list[nextIndex].date)}`,
|
||||
duration: 1500,
|
||||
color: 'secondary',
|
||||
position: 'bottom'
|
||||
});
|
||||
await toast.present();
|
||||
} else {
|
||||
// "quando le messe sono finite rimani sull'ultima valorizzata, non dare errore"
|
||||
const targetIndex = direction > 0 ? list.length - 1 : 0;
|
||||
this.cantiLettureService.selectedMassDate.set(list[targetIndex].date);
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: direction > 0 ? 'Nessun\'altra messa futura disponibile.' : 'Nessun\'altra messa passata disponibile.',
|
||||
duration: 1500,
|
||||
color: 'medium',
|
||||
position: 'bottom'
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
}
|
||||
|
||||
private formatCompactDate(dateStr: string): string {
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}`;
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user