chore: update TypeScript build info cache
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user