chore: update TypeScript build info cache
This commit is contained in:
parent
d6da699c54
commit
9c03f5f5fe
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
BIN
db/newtube.db
BIN
db/newtube.db
Binary file not shown.
@ -158,9 +158,20 @@ export class PlaylistsComponent {
|
|||||||
if (!pl?.id || !user?.id || pl.userId !== user.id) return;
|
if (!pl?.id || !user?.id || pl.userId !== user.id) return;
|
||||||
const ok = confirm(`Supprimer la playlist "${pl.title}" ?`);
|
const ok = confirm(`Supprimer la playlist "${pl.title}" ?`);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
this.http.delete(`${this.apiBase()}/playlists/${encodeURIComponent(pl.id)}`, { withCredentials: true }).subscribe({
|
|
||||||
next: () => this.reload(this.searchQuery().trim() || undefined),
|
this.http.delete(`${this.apiBase()}/playlists/${encodeURIComponent(pl.id)}`, {
|
||||||
error: (err) => this.error.set(err?.error?.error || err?.message || 'Échec de la suppression')
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { CommonModule } from '@angular/common';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { PlaylistsService, Playlist } from '../../../../services/playlists.service';
|
import { PlaylistsService, Playlist } from '../../../../services/playlists.service';
|
||||||
import { AuthService } from '../../../../services/auth.service';
|
import { AuthService } from '../../../../services/auth.service';
|
||||||
|
import { forkJoin } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-to-playlist',
|
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();">
|
<div class="relative inline-block text-left" (click)="$event.stopPropagation();">
|
||||||
<button (click)="toggleOpen($event)"
|
<button (click)="toggleOpen($event)"
|
||||||
[disabled]="!isAuthenticated()"
|
[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-expanded]="open()"
|
||||||
[attr.aria-haspopup]="true"
|
[attr.aria-haspopup]="true"
|
||||||
[attr.title]="!isAuthenticated() ? 'Connectez-vous pour gérer vos playlists' : 'Ajouter à une playlist'">
|
[attr.title]="!isAuthenticated() ? 'Connectez-vous pour gérer vos playlists' : 'Ajouter à une playlist'">
|
||||||
@ -24,47 +25,72 @@ import { AuthService } from '../../../../services/auth.service';
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (open()) {
|
@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">
|
<!-- Backdrop -->
|
||||||
<div class="p-3 space-y-3">
|
<div class="fixed inset-0 z-40 bg-black/60 backdrop-blur-[2px]" (click)="close()"></div>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-sm font-semibold text-slate-200">Vos playlists</div>
|
<!-- Modal -->
|
||||||
<button class="text-xs text-slate-400 hover:text-slate-200" (click)="reloadPlaylists()">Rafraîchir</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
@if (loading()) {
|
<!-- Body -->
|
||||||
<div class="text-sm text-slate-400">Chargement...</div>
|
<div class="px-5 py-4 space-y-4">
|
||||||
} @else if (error()) {
|
@if (loading()) {
|
||||||
<div class="text-sm text-red-300">{{ error() }}</div>
|
<div class="text-sm text-slate-400">Chargement...</div>
|
||||||
} @else if (playlists().length === 0) {
|
} @else if (error()) {
|
||||||
<div class="text-sm text-slate-400">Aucune playlist. Créez-en une ci-dessous.</div>
|
<div class="text-sm text-red-300">{{ error() }}</div>
|
||||||
} @else {
|
} @else if (playlists().length === 0) {
|
||||||
<div class="max-h-48 overflow-auto space-y-2">
|
<div class="text-sm text-slate-400">Aucune playlist. Créez-en une ci-dessous.</div>
|
||||||
@for (pl of playlists(); track pl.id) {
|
} @else {
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="max-h-[42vh] overflow-auto divide-y divide-slate-800 rounded-md border border-slate-800">
|
||||||
<div class="min-w-0">
|
@for (pl of playlists(); track pl.id) {
|
||||||
<div class="truncate text-sm text-slate-200">{{ pl.title }}</div>
|
<label class="flex items-center gap-4 px-4 py-3 hover:bg-slate-800/60 cursor-pointer">
|
||||||
<div class="text-xs text-slate-400">{{ pl.itemsCount || 0 }} vidéo(s)</div>
|
<input type="checkbox" class="accent-red-500 h-4 w-4" [checked]="isSelected(pl.id)" (change)="toggleSelect(pl.id, $event)" />
|
||||||
</div>
|
<div class="min-w-0 flex-1">
|
||||||
<button class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 text-xs"
|
<div class="truncate text-sm text-slate-200">{{ pl.title }}</div>
|
||||||
[disabled]="busyAdd()"
|
<div class="text-xs text-slate-400">{{ pl.itemsCount || 0 }} vidéo(s)</div>
|
||||||
(click)="onAddTo(pl)">Ajouter</button>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="border-t border-slate-700 pt-2">
|
<!-- Create new playlist -->
|
||||||
<label class="block text-xs text-slate-400 mb-1">Nouvelle playlist</label>
|
<div class="border-t border-slate-700 pt-4">
|
||||||
<input type="text" class="w-full bg-slate-950 text-slate-100 border border-slate-700 rounded px-2 py-1 text-sm"
|
<label class="block text-xs text-slate-400 mb-2">Nouvelle playlist</label>
|
||||||
[value]="newTitle()" (input)="onTitleChange($event)" placeholder="Titre de la playlist" />
|
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-end">
|
||||||
<div class="mt-2 flex items-center justify-end gap-2">
|
<div class="flex-1">
|
||||||
<label class="flex items-center gap-2 text-xs text-slate-300">
|
<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"
|
||||||
<input type="checkbox" class="accent-red-500" [checked]="newPrivate()" (change)="onPrivateChange($event)" />
|
[value]="newTitle()" (input)="onTitleChange($event)" placeholder="Titre de la playlist" />
|
||||||
Privée
|
</div>
|
||||||
</label>
|
<label class="flex items-center gap-2 text-xs text-slate-300">
|
||||||
<button class="px-3 py-1.5 rounded bg-red-600 hover:bg-red-500 text-white text-xs font-semibold disabled:opacity-60"
|
<input type="checkbox" class="accent-red-500" [checked]="newPrivate()" (change)="onPrivateChange($event)" />
|
||||||
[disabled]="!newTitle().trim() || busyAdd()"
|
Privée
|
||||||
(click)="onQuickCreate()">Créer + Ajouter</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -95,6 +121,7 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
|
|||||||
busyAdd = signal<boolean>(false);
|
busyAdd = signal<boolean>(false);
|
||||||
newTitle = signal<string>('');
|
newTitle = signal<string>('');
|
||||||
newPrivate = signal<boolean>(true);
|
newPrivate = signal<boolean>(true);
|
||||||
|
selected = signal<Set<string>>(new Set());
|
||||||
private destroyed = false;
|
private destroyed = false;
|
||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
@ -110,6 +137,14 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
|
|||||||
if (next && this.playlists().length === 0 && !this.loading()) {
|
if (next && this.playlists().length === 0 && !this.loading()) {
|
||||||
this.reloadPlaylists();
|
this.reloadPlaylists();
|
||||||
}
|
}
|
||||||
|
if (!next) {
|
||||||
|
this.selected.set(new Set());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.open.set(false);
|
||||||
|
this.selected.set(new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadPlaylists(): void {
|
reloadPlaylists(): void {
|
||||||
@ -121,11 +156,25 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddTo(pl: Playlist): void {
|
isSelected(id: string): boolean { return this.selected().has(id); }
|
||||||
if (this.busyAdd() || !pl?.id) return;
|
|
||||||
|
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.busyAdd.set(true);
|
||||||
this.api.addVideo(pl.id, this.provider, this.videoId, this.title, this.thumbnail).subscribe({
|
const requests = Array.from(this.selected()).map((playlistId) =>
|
||||||
next: () => { this.busyAdd.set(false); this.open.set(false); },
|
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'); }
|
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;
|
const input = event.target as HTMLInputElement;
|
||||||
this.newPrivate.set(input.checked);
|
this.newPrivate.set(input.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close on ESC key
|
||||||
|
@HostListener('window:keydown.escape')
|
||||||
|
onEsc() {
|
||||||
|
if (this.open()) this.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,12 +143,18 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
|||||||
return null;
|
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 {
|
openOnProviderUrl(): string | null {
|
||||||
const p = this.provider();
|
const p = this.provider();
|
||||||
const id = this.videoId();
|
const id = this.videoId();
|
||||||
const v = this.video();
|
const v = this.video();
|
||||||
const directUrl = v?.url && /^https?:\/\//i.test(v.url) ? v.url : null;
|
const directUrl = v?.url && /^https?:\/\//i.test(v.url) ? v.url : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (p === 'youtube') return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;
|
if (p === 'youtube') return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;
|
||||||
if (p === 'dailymotion') return `https://www.dailymotion.com/video/${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 (directUrl) return directUrl;
|
||||||
}
|
}
|
||||||
if (p === 'rumble') {
|
if (p === 'rumble') {
|
||||||
if (directUrl) return directUrl;
|
const rumbleId = this.getRumbleRouteId();
|
||||||
if (id) return `https://rumble.com/${encodeURIComponent(id)}`;
|
return rumbleId ? `https://rumble.com/${rumbleId}` : null;
|
||||||
}
|
}
|
||||||
return directUrl || null;
|
return directUrl;
|
||||||
} catch {
|
} 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[]);
|
availableQualities = computed(() => this.sortedStreams().map(s => s.quality).filter((q, i, arr) => q && arr.indexOf(q) === i) as string[]);
|
||||||
videoSource = computed(() => {
|
videoSource = computed(() => {
|
||||||
const qual = this.selectedQuality();
|
const qual = this.selectedQuality();
|
||||||
@ -264,13 +286,30 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
|||||||
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
||||||
}
|
}
|
||||||
if (p === 'rumble') {
|
if (p === 'rumble') {
|
||||||
// Prefer backend-provided embed URL (includes required pub/query params)
|
const currentVideo = this.video();
|
||||||
const fromBackend = this.rumbleEmbed();
|
const videoUrl = currentVideo?.url;
|
||||||
const u = fromBackend && /^https?:\/\//i.test(fromBackend)
|
|
||||||
? fromBackend
|
// Extraire l'ID de la vidéo Rumble
|
||||||
: `https://rumble.com/embed/${encodeURIComponent(id)}/?autoplay=2&muted=1`;
|
let videoId = '';
|
||||||
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
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 {}
|
} catch {}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ export interface Video {
|
|||||||
views: number;
|
views: number;
|
||||||
uploaded: number;
|
uploaded: number;
|
||||||
videoId: string;
|
videoId: string;
|
||||||
|
providerUrl?: string; // URL complète de la vidéo sur le site du fournisseur
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoDetail extends Video {
|
export interface VideoDetail extends Video {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user