chore: update TypeScript build info cache

This commit is contained in:
2025-09-18 22:20:52 -04:00
parent bcfb46ef7a
commit 41255a8126
22 changed files with 529 additions and 665 deletions
@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { PlaylistsService, Playlist, PlaylistItem } from '../../../services/playlists.service';
import { AuthService } from '../../../services/auth.service';
@Component({
selector: 'app-playlist-detail',
@@ -11,7 +13,7 @@ import { PlaylistsService, Playlist, PlaylistItem } from '../../../services/play
<div class="container mx-auto p-4 sm:p-6">
<div class="flex items-center justify-between gap-3 mb-4">
<a routerLink="/library/playlists" class="text-slate-300 hover:text-slate-100">← Retour</a>
<div class="flex items-center gap-2" *ngIf="dirty()">
<div class="flex items-center gap-2" *ngIf="dirty() && isOwner()">
<button (click)="saveOrder()" [disabled]="saving()" class="px-4 py-2 rounded bg-red-600 hover:bg-red-500 text-white font-semibold">
{{ saving() ? 'Enregistrement...' : 'Enregistrer l\'ordre' }}
</button>
@@ -41,10 +43,7 @@ import { PlaylistsService, Playlist, PlaylistItem } from '../../../services/play
<div class="flex flex-col sm:flex-row gap-4 mb-6">
<div class="w-full sm:w-64">
<div class="rounded overflow-hidden border border-slate-700 bg-slate-800">
<img *ngIf="playlist()?.thumbnail; else noThumb" [src]="playlist()!.thumbnail!" alt="" class="w-full h-40 object-cover"/>
<ng-template #noThumb>
<div class="w-full h-40 bg-gradient-to-br from-slate-700 to-slate-800"></div>
</ng-template>
<img [src]="playlist()?.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="playlist()?.title || 'Playlist'" class="w-full h-40 object-cover"/>
</div>
</div>
<div class="flex-1">
@@ -60,16 +59,13 @@ import { PlaylistsService, Playlist, PlaylistItem } from '../../../services/play
<div class="space-y-3">
<div *ngFor="let it of items(); let i = index" class="flex items-center gap-3 p-2 rounded border border-slate-700 bg-slate-800/60">
<div class="w-28 flex-shrink-0">
<img *ngIf="it.thumbnail; else noThumb2" [src]="it.thumbnail!" alt="" class="w-28 h-16 object-cover rounded">
<ng-template #noThumb2>
<div class="w-28 h-16 bg-slate-700 rounded"></div>
</ng-template>
<img [src]="it.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="it.title || 'Video thumbnail'" class="w-28 h-16 object-cover rounded">
</div>
<div class="flex-1">
<a [routerLink]="['/watch', it.videoId]" [queryParams]="{ p: it.provider }" class="font-semibold text-slate-100 hover:text-red-400 line-clamp-2">{{ it.title || it.videoId }}</a>
<div class="text-xs text-slate-400 mt-1">{{ it.provider }} • Pos. {{ i + 1 }}</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2" *ngIf="isOwner()">
<button (click)="moveUp(i)" [disabled]="i===0" class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 text-sm">↑</button>
<button (click)="moveDown(i)" [disabled]="i===items().length-1" class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 text-sm">↓</button>
<button (click)="removeItem(it)" class="px-2 py-1 rounded bg-red-700 hover:bg-red-600 text-white text-sm">Retirer</button>
@@ -82,8 +78,16 @@ import { PlaylistsService, Playlist, PlaylistItem } from '../../../services/play
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlaylistDetailComponent {
// Default image path
readonly DEFAULT_PLAYLIST_IMAGE = '/images/default_playlist.png';
private route = inject(ActivatedRoute);
private router = inject(Router);
private http = inject(HttpClient);
private api = inject(PlaylistsService);
private auth = inject(AuthService);
// Expose a bound accessor to avoid losing 'this' context
currentUser = () => this.auth.currentUser();
loading = signal<boolean>(true);
error = signal<string | null>(null);
@@ -91,6 +95,7 @@ export class PlaylistDetailComponent {
items = signal<PlaylistItem[]>([]);
dirty = signal<boolean>(false);
saving = signal<boolean>(false);
isOwner = signal<boolean>(false);
private id = signal<string>('');
@@ -103,14 +108,24 @@ export class PlaylistDetailComponent {
load(id: string) {
this.loading.set(true);
this.error.set(null);
const onLoad = (pl: any) => {
this.playlist.set(pl);
this.items.set((pl.items || []).slice().sort((a: PlaylistItem, b: PlaylistItem) => a.position - b.position));
const user = this.currentUser();
const uid = user?.id;
this.isOwner.set(Boolean(uid && pl && pl.userId === uid));
this.loading.set(false);
this.dirty.set(false);
};
this.api.get(id, 1000, 0).subscribe({
next: (pl) => {
this.playlist.set(pl);
this.items.set((pl.items || []).slice().sort((a, b) => a.position - b.position));
this.loading.set(false);
this.dirty.set(false);
},
error: (err) => { this.error.set(err?.error?.error || err?.message || 'Erreur de chargement'); this.loading.set(false); }
next: onLoad,
error: (_err) => {
// Fallback to public view
this.api.view(id, 1000, 0).subscribe({
next: onLoad,
error: (err2) => { this.error.set(err2?.error?.error || err2?.message || 'Erreur de chargement'); this.loading.set(false); }
});
}
});
}
@@ -135,7 +150,7 @@ export class PlaylistDetailComponent {
}
saveOrder() {
if (!this.dirty() || !this.id()) return;
if (!this.isOwner() || !this.dirty() || !this.id()) return;
this.saving.set(true);
const order = this.items().map(it => it.id);
this.api.reorder(this.id(), order).subscribe({
@@ -145,6 +160,7 @@ export class PlaylistDetailComponent {
}
removeItem(it: PlaylistItem) {
if (!this.isOwner()) return;
const ok = confirm('Retirer cette vidéo de la playlist ?');
if (!ok) return;
this.api.removeVideo(this.id(), it.provider, it.videoId).subscribe({
@@ -37,13 +37,7 @@
<div class="bg-slate-800/70 rounded-lg overflow-hidden border border-slate-700 flex flex-col">
<a [routerLink]="['/library/playlists', pl.id]" class="block group">
<div class="relative">
@if (pl.thumbnail) {
<img [src]="pl.thumbnail!" alt="" class="w-full h-40 object-cover group-hover:opacity-90 transition" />
} @else {
<div class="w-full h-40 bg-gradient-to-br from-slate-700 to-slate-800 flex items-center justify-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" viewBox="0 0 24 24" fill="currentColor"><path d="M4 5a2 2 0 00-2 2v7.5A2.5 2.5 0 004.5 17H20a2 2 0 002-2V7a2 2 0 00-2-2H4zm0 2h16v8H4.5a.5.5 0 01-.5-.5V7z"/><path d="M12 8.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z"/></svg>
</div>
}
<img [src]="pl.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="pl.title" class="w-full h-40 object-cover group-hover:opacity-90 transition" />
</div>
</a>
<div class="p-4 flex-1 flex flex-col">
@@ -58,8 +52,10 @@
<span>MAJ: {{ (pl.updatedAt || '').slice(0,10) }}</span>
</div>
<div class="mt-3 flex items-center gap-2">
<button (click)="openEdit(pl)" class="px-3 py-1.5 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 text-sm">Modifier</button>
<button (click)="deletePlaylist(pl)" class="px-3 py-1.5 rounded bg-red-700 hover:bg-red-600 text-white text-sm">Supprimer</button>
@if (currentUser() && pl.userId === currentUser()?.id) {
<button (click)="openEdit(pl)" class="px-3 py-1.5 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 text-sm">Modifier</button>
<button (click)="deletePlaylist(pl)" class="px-3 py-1.5 rounded bg-red-700 hover:bg-red-600 text-white text-sm">Supprimer</button>
}
</div>
</div>
</div>
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { RouterLink } from '@angular/router';
import { AuthService } from '../../../services/auth.service';
@Component({
selector: 'app-library-playlists',
@@ -12,13 +13,20 @@ import { RouterLink } from '@angular/router';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlaylistsComponent {
// Default image path
readonly DEFAULT_PLAYLIST_IMAGE = '/images/default_playlist.png';
private http = inject(HttpClient);
private auth = inject(AuthService);
// Expose a bound accessor to avoid losing 'this' context when used in template
currentUser = () => this.auth.currentUser();
// UI state
loading = signal<boolean>(true);
error = signal<string | null>(null);
searchQuery = signal<string>('');
playlists = signal<Array<{ id: string; title: string; description?: string | null; thumbnail?: string | null; isPrivate: number | boolean; createdAt: string; updatedAt: string; itemsCount: number }>>([]);
playlists = signal<Array<{ id: string; userId?: string; title: string; description?: string | null; thumbnail?: string | null; isPrivate: number | boolean; createdAt: string; updatedAt: string; itemsCount: number }>>([]);
// Modal state
modalOpen = signal<boolean>(false);
@@ -45,10 +53,32 @@ export class PlaylistsComponent {
this.error.set(null);
const params: any = { limit: 200 };
if (q && q.trim()) params.q = q.trim();
this.http.get<any[]>(`${this.apiBase()}/playlists`, { params, withCredentials: true }).subscribe({
next: (rows) => { this.playlists.set(rows || []); this.loading.set(false); },
const own$ = this.http.get<any[]>(`${this.apiBase()}/playlists`, { params, withCredentials: true });
const pub$ = this.http.get<any[]>(`${this.apiBase()}/playlists/public`, { params, withCredentials: true });
let own: any[] = [];
let pub: any[] = [];
let ownErr: any = null;
// Fetch own (may 401 for anonymous)
own$.subscribe({
next: (rows) => { own = rows || []; tryMerge(); },
error: () => { own = []; ownErr = true; tryMerge(); }
});
// Fetch public
pub$.subscribe({
next: (rows) => { pub = rows || []; tryMerge(); },
error: (err) => { this.error.set(err?.error?.error || err?.message || 'Erreur de chargement'); this.loading.set(false); }
});
const tryMerge = () => {
// Wait until we got public at least (own may be empty or errored)
if (pub === null) return;
const map = new Map<string, any>();
for (const p of pub) if (p && p.id) map.set(p.id, p);
for (const o of own) if (o && o.id) map.set(o.id, o);
const merged = Array.from(map.values());
this.playlists.set(merged);
this.loading.set(false);
};
}
onSearchChange(event: Event) {
@@ -88,6 +118,9 @@ export class PlaylistsComponent {
}
openEdit(pl: any) {
// Only owners can edit
const user = this.currentUser();
if (!user?.id || pl.userId !== user.id) return;
this.editId.set(pl.id);
this.formTitle.set(pl.title || '');
this.formDescription.set(pl.description || '');
@@ -121,7 +154,8 @@ export class PlaylistsComponent {
}
deletePlaylist(pl: any) {
if (!pl?.id) return;
const user = this.currentUser();
if (!pl?.id || !user?.id || pl.userId !== user.id) return;
const ok = confirm(`Supprimer la playlist "${pl.title}" ?`);
if (!ok) return;
this.http.delete(`${this.apiBase()}/playlists/${encodeURIComponent(pl.id)}`, { withCredentials: true }).subscribe({