chore: update TypeScript build info cache

This commit is contained in:
Bruno Charest 2025-09-19 12:52:02 -04:00
parent d6da699c54
commit 9c03f5f5fe
6 changed files with 165 additions and 59 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -158,9 +158,20 @@ export class PlaylistsComponent {
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({
next: () => this.reload(this.searchQuery().trim() || undefined),
error: (err) => this.error.set(err?.error?.error || err?.message || 'Échec de la suppression')
this.http.delete(`${this.apiBase()}/playlists/${encodeURIComponent(pl.id)}`, {
withCredentials: true,
responseType: 'text' // Handle empty response
}).subscribe({
next: () => {
// Remove the playlist from the local list immediately for better UX
this.playlists.set(this.playlists().filter(p => p.id !== pl.id));
},
error: (err) => {
this.error.set(err?.error?.error || err?.message || 'Échec de la suppression');
// Reload to ensure consistency with the server
this.reload(this.searchQuery().trim() || undefined);
}
});
}
}

View File

@ -1,8 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, SimpleChanges, inject, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, inject, signal, HostListener } 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';
import { forkJoin } from 'rxjs';
@Component({
selector: 'app-add-to-playlist',
@ -12,7 +13,7 @@ import { AuthService } from '../../../../services/auth.service';
<div class="relative inline-block text-left" (click)="$event.stopPropagation();">
<button (click)="toggleOpen($event)"
[disabled]="!isAuthenticated()"
class="flex items-center space-x-2 bg-slate-700 hover:bg-slate-600 disabled:opacity-60 disabled:cursor-not-allowed px-4 py-2 rounded-full transition text-slate-200 font-semibold"
class="flex items-center space-x-2 bg-slate-700 hover:bg-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 disabled:opacity-60 disabled:cursor-not-allowed px-4 py-2 rounded-full transition text-slate-200 font-semibold"
[attr.aria-expanded]="open()"
[attr.aria-haspopup]="true"
[attr.title]="!isAuthenticated() ? 'Connectez-vous pour gérer vos playlists' : 'Ajouter à une playlist'">
@ -24,47 +25,72 @@ import { AuthService } from '../../../../services/auth.service';
</button>
@if (open()) {
<div class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-80 rounded-md bg-slate-900 border border-slate-700 shadow-2xl 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>
<!-- Backdrop -->
<div class="fixed inset-0 z-40 bg-black/60 backdrop-blur-[2px]" (click)="close()"></div>
<!-- Modal -->
<div class="fixed inset-0 z-50 grid place-items-center" aria-modal="true" role="dialog" [attr.aria-labelledby]="'addToPlaylistTitle'">
<div class="w-[min(92vw,720px)] max-h-[min(80vh,700px)] rounded-xl bg-slate-900 border border-slate-700 shadow-2xl focus:outline-none overflow-hidden" (click)="$event.stopPropagation()">
<!-- Header -->
<div class="flex items-center justify-between px-5 py-4 border-b border-slate-700">
<div id="addToPlaylistTitle" class="text-base font-semibold text-slate-100">Vos playlists</div>
<div class="flex items-center gap-3">
<button class="text-xs text-slate-400 hover:text-slate-200" (click)="reloadPlaylists()">Rafraîchir</button>
<button class="h-8 w-8 grid place-items-center rounded-md text-slate-300 hover:text-white hover:bg-slate-700/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60" (click)="close()" aria-label="Fermer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5"><path d="M6.225 4.811a1 1 0 011.414 0L12 9.172l4.361-4.361a1 1 0 011.415 1.415L13.415 10.586l4.361 4.361a1 1 0 01-1.415 1.415L12 12l-4.361 4.361a1 1 0 01-1.414-1.415l4.36-4.36-4.36-4.36a1 1 0 010-1.415z"/></svg>
</button>
</div>
</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>
}
<!-- Body -->
<div class="px-5 py-4 space-y-4">
@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-[42vh] overflow-auto divide-y divide-slate-800 rounded-md border border-slate-800">
@for (pl of playlists(); track pl.id) {
<label class="flex items-center gap-4 px-4 py-3 hover:bg-slate-800/60 cursor-pointer">
<input type="checkbox" class="accent-red-500 h-4 w-4" [checked]="isSelected(pl.id)" (change)="toggleSelect(pl.id, $event)" />
<div class="min-w-0 flex-1">
<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>
</label>
}
</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>
<!-- Create new playlist -->
<div class="border-t border-slate-700 pt-4">
<label class="block text-xs text-slate-400 mb-2">Nouvelle playlist</label>
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-end">
<div class="flex-1">
<input type="text" class="w-full bg-slate-950 text-slate-100 border border-slate-700 rounded px-3 py-2 text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500/60"
[value]="newTitle()" (input)="onTitleChange($event)" placeholder="Titre de la playlist" />
</div>
<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-4 py-2 rounded-md bg-red-600 hover:bg-red-500 text-white text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
[disabled]="!newTitle().trim() || busyAdd()"
(click)="onQuickCreate()">Créer + Ajouter</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-5 py-4 border-t border-slate-700 bg-slate-900/60 flex items-center justify-between">
<div class="text-xs text-slate-400">{{ selectedCount() }} sélectionnée(s)</div>
<div class="flex items-center gap-3">
<button class="px-4 py-2 rounded-md bg-slate-700 hover:bg-slate-600 text-slate-100 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400/60" (click)="close()">Annuler</button>
<button class="px-4 py-2 rounded-md bg-red-600 hover:bg-red-500 text-white text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
[disabled]="busyAdd() || selectedCount() === 0"
(click)="onAddSelected()">Ajouter</button>
</div>
</div>
</div>
@ -95,6 +121,7 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
busyAdd = signal<boolean>(false);
newTitle = signal<string>('');
newPrivate = signal<boolean>(true);
selected = signal<Set<string>>(new Set());
private destroyed = false;
ngOnInit(): void {}
@ -110,6 +137,14 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
if (next && this.playlists().length === 0 && !this.loading()) {
this.reloadPlaylists();
}
if (!next) {
this.selected.set(new Set());
}
}
close(): void {
this.open.set(false);
this.selected.set(new Set());
}
reloadPlaylists(): void {
@ -121,11 +156,25 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
});
}
onAddTo(pl: Playlist): void {
if (this.busyAdd() || !pl?.id) return;
isSelected(id: string): boolean { return this.selected().has(id); }
selectedCount(): number { return this.selected().size; }
toggleSelect(id: string, event: Event): void {
const input = event.target as HTMLInputElement;
const next = new Set(this.selected());
if (input.checked) next.add(id); else next.delete(id);
this.selected.set(next);
}
onAddSelected(): void {
if (this.busyAdd() || this.selected().size === 0) 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); },
const requests = Array.from(this.selected()).map((playlistId) =>
this.api.addVideo(playlistId, this.provider, this.videoId, this.title, this.thumbnail)
);
forkJoin(requests).subscribe({
next: () => { this.busyAdd.set(false); this.close(); },
error: (err) => { this.busyAdd.set(false); this.error.set(err?.error?.error || err?.message || 'Échec de l\'ajout'); }
});
}
@ -155,4 +204,10 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
const input = event.target as HTMLInputElement;
this.newPrivate.set(input.checked);
}
// Close on ESC key
@HostListener('window:keydown.escape')
onEsc() {
if (this.open()) this.close();
}
}

View File

@ -143,12 +143,18 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
return null;
}
// Generic "open on provider" URL
// Méthode simplifiée pour obtenir l'ID Rumble (toujours l'ID de la route)
private getRumbleRouteId(): string {
return this.route.snapshot.params['id'] || '';
}
// Generic "open on provider" URL (returns string for external links)
openOnProviderUrl(): string | null {
const p = this.provider();
const id = this.videoId();
const v = this.video();
const directUrl = v?.url && /^https?:\/\//i.test(v.url) ? v.url : null;
try {
if (p === 'youtube') return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;
if (p === 'dailymotion') return `https://www.dailymotion.com/video/${encodeURIComponent(id)}`;
@ -163,14 +169,30 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
if (directUrl) return directUrl;
}
if (p === 'rumble') {
if (directUrl) return directUrl;
if (id) return `https://rumble.com/${encodeURIComponent(id)}`;
const rumbleId = this.getRumbleRouteId();
return rumbleId ? `https://rumble.com/${rumbleId}` : null;
}
return directUrl || null;
return directUrl;
} catch {
return directUrl || null;
return directUrl;
}
}
// Get secure embed URL (returns SafeResourceUrl for iframe src)
getEmbedUrl(): SafeResourceUrl | null {
const p = this.provider();
const id = this.videoId();
if (p === 'rumble') {
const rumbleId = this.getRumbleRouteId();
if (rumbleId) {
return this.sanitizer.bypassSecurityTrustResourceUrl(
`https://rumble.com/embed/${rumbleId}/?pub=4&autoplay=1`
);
}
}
return null;
}
availableQualities = computed(() => this.sortedStreams().map(s => s.quality).filter((q, i, arr) => q && arr.indexOf(q) === i) as string[]);
videoSource = computed(() => {
const qual = this.selectedQuality();
@ -264,13 +286,30 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}
if (p === 'rumble') {
// Prefer backend-provided embed URL (includes required pub/query params)
const fromBackend = this.rumbleEmbed();
const u = fromBackend && /^https?:\/\//i.test(fromBackend)
? fromBackend
: `https://rumble.com/embed/${encodeURIComponent(id)}/?autoplay=2&muted=1`;
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
const currentVideo = this.video();
const videoUrl = currentVideo?.url;
// Extraire l'ID de la vidéo Rumble
let videoId = '';
if (videoUrl) {
const match = videoUrl.match(/\/(v[\w-]+)(?:\.html|$)/i);
if (match?.[1]) {
videoId = match[1];
}
}
// Si on n'a pas pu extraire l'ID de l'URL, on utilise l'ID de la route
if (!videoId && id) {
videoId = id.replace(/^v/, '');
}
// Construire l'URL d'embed officielle de Rumble
if (videoId) {
const embedUrl = `https://rumble.com/embed/${videoId}/?pub=4&autoplay=1`;
return this.sanitizer.bypassSecurityTrustResourceUrl(embedUrl);
}
}
return null;
} catch {}
return null;
});

View File

@ -13,6 +13,7 @@ export interface Video {
views: number;
uploaded: number;
videoId: string;
providerUrl?: string; // URL complète de la vidéo sur le site du fournisseur
}
export interface VideoDetail extends Video {