chore: update Angular TypeScript build info cache

This commit is contained in:
2025-09-17 22:29:57 -04:00
parent f3a78b7d7e
commit fff9b88663
15 changed files with 1002 additions and 11 deletions
@@ -0,0 +1,158 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, SimpleChanges, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { PlaylistsService, Playlist } from '../../../../services/playlists.service';
import { AuthService } from '../../../../services/auth.service';
@Component({
selector: 'app-add-to-playlist',
standalone: true,
imports: [CommonModule, HttpClientModule],
template: `
<div class="relative inline-block text-left" (click)="$event.stopPropagation();">
<button (click)="toggleOpen($event)"
[disabled]="!isAuthenticated()"
class="transition-colors duration-200 p-1 rounded-full hover:bg-slate-700/50 flex items-center justify-center text-slate-300 disabled:opacity-60"
[attr.aria-expanded]="open()"
[attr.aria-haspopup]="true"
[attr.title]="!isAuthenticated() ? 'Connectez-vous pour gérer vos playlists' : 'Ajouter à une playlist'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 6a2 2 0 012-2h12a1 1 0 010 2H6v10a1 1 0 11-2 0V6z" />
<path d="M14 11a1 1 0 011-1h5a1 1 0 110 2h-5a1 1 0 01-1-1zM14 15a1 1 0 011-1h5a1 1 0 110 2h-5a1 1 0 01-1-1zM10 11a1 1 0 112 0v1h1a1 1 0 110 2h-1v1a1 1 0 11-2 0v-1H9a1 1 0 110-2h1v-1z" />
</svg>
<span class="sr-only">Ajouter à une playlist</span>
</button>
@if (open()) {
<div class="absolute right-0 mt-2 w-72 origin-top-right rounded-md bg-slate-900 border border-slate-700 shadow-lg focus:outline-none z-50">
<div class="p-3 space-y-3">
<div class="flex items-center justify-between">
<div class="text-sm font-semibold text-slate-200">Vos playlists</div>
<button class="text-xs text-slate-400 hover:text-slate-200" (click)="reloadPlaylists()">Rafraîchir</button>
</div>
@if (loading()) {
<div class="text-sm text-slate-400">Chargement...</div>
} @else if (error()) {
<div class="text-sm text-red-300">{{ error() }}</div>
} @else if (playlists().length === 0) {
<div class="text-sm text-slate-400">Aucune playlist. Créez-en une ci-dessous.</div>
} @else {
<div class="max-h-48 overflow-auto space-y-2">
@for (pl of playlists(); track pl.id) {
<div class="flex items-center justify-between gap-2">
<div class="min-w-0">
<div class="truncate text-sm text-slate-200">{{ pl.title }}</div>
<div class="text-xs text-slate-400">{{ pl.itemsCount || 0 }} vidéo(s)</div>
</div>
<button class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 text-xs"
[disabled]="busyAdd()"
(click)="onAddTo(pl)">Ajouter</button>
</div>
}
</div>
}
<div class="border-t border-slate-700 pt-2">
<label class="block text-xs text-slate-400 mb-1">Nouvelle playlist</label>
<input type="text" class="w-full bg-slate-950 text-slate-100 border border-slate-700 rounded px-2 py-1 text-sm"
[value]="newTitle()" (input)="onTitleChange($event)" placeholder="Titre de la playlist" />
<div class="mt-2 flex items-center justify-end gap-2">
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="checkbox" class="accent-red-500" [checked]="newPrivate()" (change)="onPrivateChange($event)" />
Privée
</label>
<button class="px-3 py-1.5 rounded bg-red-600 hover:bg-red-500 text-white text-xs font-semibold disabled:opacity-60"
[disabled]="!newTitle().trim() || busyAdd()"
(click)="onQuickCreate()">Créer + Ajouter</button>
</div>
</div>
</div>
</div>
}
</div>
`,
styles: [
`
.sr-only { position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0; }
`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AddToPlaylistComponent implements OnInit, OnDestroy {
private api = inject(PlaylistsService);
private auth = inject(AuthService);
@Input() provider: string = 'youtube';
@Input() videoId: string = '';
@Input() title?: string;
@Input() thumbnail?: string;
open = signal<boolean>(false);
loading = signal<boolean>(false);
error = signal<string | null>(null);
playlists = signal<Playlist[]>([]);
busyAdd = signal<boolean>(false);
newTitle = signal<string>('');
newPrivate = signal<boolean>(true);
private destroyed = false;
ngOnInit(): void {}
ngOnDestroy(): void { this.destroyed = true; }
isAuthenticated(): boolean { return !!this.auth.currentUser(); }
toggleOpen(ev: Event): void {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
if (!this.isAuthenticated()) return;
const next = !this.open();
this.open.set(next);
if (next && this.playlists().length === 0 && !this.loading()) {
this.reloadPlaylists();
}
}
reloadPlaylists(): void {
this.loading.set(true);
this.error.set(null);
this.api.list(200, 0).subscribe({
next: (rows) => { if (!this.destroyed) { this.playlists.set(rows || []); this.loading.set(false); } },
error: (err) => { if (!this.destroyed) { this.error.set(err?.error?.error || err?.message || 'Erreur'); this.loading.set(false); } }
});
}
onAddTo(pl: Playlist): void {
if (this.busyAdd() || !pl?.id) return;
this.busyAdd.set(true);
this.api.addVideo(pl.id, this.provider, this.videoId, this.title, this.thumbnail).subscribe({
next: () => { this.busyAdd.set(false); this.open.set(false); },
error: (err) => { this.busyAdd.set(false); this.error.set(err?.error?.error || err?.message || 'Échec de l\'ajout'); }
});
}
onQuickCreate(): void {
const t = this.newTitle().trim();
if (!t) return;
if (this.busyAdd()) return;
this.busyAdd.set(true);
this.api.create(t, undefined, undefined, this.newPrivate()).subscribe({
next: (pl) => {
this.api.addVideo(pl.id, this.provider, this.videoId, this.title, this.thumbnail).subscribe({
next: () => { this.busyAdd.set(false); this.open.set(false); this.newTitle.set(''); },
error: (err) => { this.busyAdd.set(false); this.error.set(err?.error?.error || err?.message || 'Échec de l\'ajout'); }
});
},
error: (err) => { this.busyAdd.set(false); this.error.set(err?.error?.error || err?.message || 'Échec de la création'); }
});
}
onTitleChange(event: Event) {
const input = event.target as HTMLInputElement;
this.newTitle.set(input.value);
}
onPrivateChange(event: Event) {
const input = event.target as HTMLInputElement;
this.newPrivate.set(input.checked);
}
}