chore: update Angular build cache and TypeScript definitions
This commit is contained in:
@@ -1,10 +1,36 @@
|
||||
<div class="container mx-auto p-4 sm:p-6 max-w-6xl">
|
||||
<h2 class="text-3xl font-bold mb-8 text-slate-100 border-l-4 border-red-500 pl-4 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Historique
|
||||
</h2>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
|
||||
<h2 class="text-3xl font-bold text-slate-100 border-l-4 border-red-500 pl-4 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Historique
|
||||
</h2>
|
||||
<div class="relative w-full md:w-96">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
[ngModel]="searchQuery()"
|
||||
(ngModelChange)="searchQuery.set($event)"
|
||||
class="block w-full pl-10 pr-3 py-2 border border-slate-600 rounded-lg bg-slate-700 text-slate-100 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-150"
|
||||
placeholder="Rechercher dans l'historique..."
|
||||
>
|
||||
<button
|
||||
*ngIf="searchQuery()"
|
||||
(click)="searchQuery.set('')"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-300 transition-colors"
|
||||
aria-label="Effacer la recherche"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Search History Section -->
|
||||
@@ -16,46 +42,132 @@
|
||||
</svg>
|
||||
Historique des recherches
|
||||
</h3>
|
||||
<span class="text-xs bg-slate-700 text-slate-300 px-2 py-1 rounded-full">
|
||||
{{ searchHistory().length }} recherche(s)
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs bg-slate-700 text-slate-300 px-2 py-1 rounded-full">
|
||||
{{ searchHistory().length }} recherche(s)
|
||||
<span *ngIf="searchQuery()">
|
||||
• {{ filteredSearchHistory().length }} résultat(s)
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="searchHistory().length === 0" class="text-slate-400 text-center py-4">
|
||||
Aucune recherche récente
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3">
|
||||
<li *ngFor="let s of searchHistory()"
|
||||
class="group bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/search']" [queryParams]="{ q: s.query, provider: getSearchProvider(s).id }"
|
||||
class="block p-4 hover:no-underline">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="text-slate-100 font-medium group-hover:text-blue-400 transition-colors">
|
||||
{{ s.query }}
|
||||
</div>
|
||||
<div class="flex items-center mt-1 text-xs text-slate-400">
|
||||
<span class="inline-flex items-center bg-slate-800/80 px-2 py-0.5 rounded mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
{{ getSearchProvider(s).name }}
|
||||
</span>
|
||||
<span class="text-slate-500">
|
||||
{{ formatDate(s.created_at) }}
|
||||
</span>
|
||||
<ng-container *ngIf="searchQuery(); else normalSearchHistory">
|
||||
<!-- Mode recherche -->
|
||||
<div *ngIf="isLoading()" class="text-center py-6">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
|
||||
<p class="mt-2 text-slate-400">Recherche en cours...</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isLoading() && filteredSearchHistory().length === 0" class="text-slate-400 text-center py-6">
|
||||
Aucun résultat trouvé dans l'historique des recherches pour "{{ searchQuery() }}"
|
||||
</div>
|
||||
|
||||
<ul *ngIf="!isLoading() && filteredSearchHistory().length > 0" class="space-y-3">
|
||||
<li *ngFor="let s of filteredSearchHistory()"
|
||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/search']" [queryParams]="{ q: s.query, provider: getSearchProvider(s).id }"
|
||||
class="block p-4 pr-16 hover:no-underline">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-slate-400 mt-0.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-slate-100 font-medium group-hover:text-blue-400 transition-colors">
|
||||
{{ s.query }}
|
||||
</div>
|
||||
<div class="flex items-center mt-1 text-xs text-slate-400">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
||||
[ngStyle]="getProviderColors(getSearchProvider(s).id)"
|
||||
[ngClass]="{
|
||||
'bg-red-600/15 text-red-400 border-red-500/30': getSearchProvider(s).id === 'youtube',
|
||||
'bg-sky-600/15 text-sky-300 border-sky-500/30': getSearchProvider(s).id === 'vimeo',
|
||||
'bg-blue-600/15 text-blue-300 border-blue-500/30': getSearchProvider(s).id === 'dailymotion',
|
||||
'bg-amber-600/15 text-amber-300 border-amber-500/30': getSearchProvider(s).id === 'peertube',
|
||||
'bg-green-600/15 text-green-300 border-green-500/30': getSearchProvider(s).id === 'rumble',
|
||||
'bg-slate-800/80 text-slate-300 border-slate-700': !['youtube', 'vimeo', 'dailymotion', 'peertube', 'rumble'].includes(getSearchProvider(s).id)
|
||||
}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
{{ getSearchProvider(s).name }}
|
||||
</span>
|
||||
<span class="text-slate-500">
|
||||
{{ formatDate(s.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="text-slate-400 hover:text-blue-400 p-1 -mr-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
<button (click)="onDeleteSearch(s); $event.preventDefault(); $event.stopPropagation();"
|
||||
[disabled]="deletingSearchId() === s.id"
|
||||
class="absolute bottom-2 right-2 inline-flex items-center gap-1 px-2 py-1 rounded-md bg-red-600/90 hover:bg-red-500/90 text-white text-xs disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-3 w-3">
|
||||
<path d="M9 3a1 1 0 00-1 1v1H5a1 1 0 100 2h14a1 1 0 100-2h-3V4a1 1 0 00-1-1H9zM6 9a1 1 0 011 1v8a2 2 0 002 2h6a2 2 0 002-2v-8a1 1 0 112 0v8a4 4 0 01-4 4H9a4 4 0 01-4-4v-8a1 1 0 011-1z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Supprimer</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #normalSearchHistory>
|
||||
<!-- Mode affichage normal -->
|
||||
<div *ngIf="searchHistory().length === 0" class="text-slate-400 text-center py-4">
|
||||
Aucune recherche récente
|
||||
</div>
|
||||
|
||||
<ul *ngIf="searchHistory().length > 0" class="space-y-3">
|
||||
<li *ngFor="let s of searchHistory()"
|
||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/search']" [queryParams]="{ q: s.query, provider: getSearchProvider(s).id }"
|
||||
class="block p-4 pr-16 hover:no-underline">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-slate-400 mt-0.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-slate-100 font-medium group-hover:text-blue-400 transition-colors">
|
||||
{{ s.query }}
|
||||
</div>
|
||||
<div class="flex items-center mt-1 text-xs text-slate-400">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
||||
[ngStyle]="getProviderColors(getSearchProvider(s).id)"
|
||||
[ngClass]="{
|
||||
'bg-red-600/15 text-red-400 border-red-500/30': getSearchProvider(s).id === 'youtube',
|
||||
'bg-sky-600/15 text-sky-300 border-sky-500/30': getSearchProvider(s).id === 'vimeo',
|
||||
'bg-blue-600/15 text-blue-300 border-blue-500/30': getSearchProvider(s).id === 'dailymotion',
|
||||
'bg-amber-600/15 text-amber-300 border-amber-500/30': getSearchProvider(s).id === 'peertube',
|
||||
'bg-green-600/15 text-green-300 border-green-500/30': getSearchProvider(s).id === 'rumble',
|
||||
'bg-slate-800/80 text-slate-300 border-slate-700': !['youtube', 'vimeo', 'dailymotion', 'peertube', 'rumble'].includes(getSearchProvider(s).id)
|
||||
}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
{{ getSearchProvider(s).name }}
|
||||
</span>
|
||||
<span class="text-slate-500">
|
||||
{{ formatDate(s.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<button (click)="onDeleteSearch(s); $event.preventDefault(); $event.stopPropagation();"
|
||||
[disabled]="deletingSearchId() === s.id"
|
||||
class="absolute bottom-2 right-2 inline-flex items-center gap-1 px-2 py-1 rounded-md bg-red-600/90 hover:bg-red-500/90 text-white text-xs disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-3 w-3">
|
||||
<path d="M9 3a1 1 0 00-1 1v1H5a1 1 0 100 2h14a1 1 0 100-2h-3V4a1 1 0 00-1-1H9zM6 9a1 1 0 011 1v8a2 2 0 002 2h6a2 2 0 002-2v-8a1 1 0 112 0v8a4 4 0 01-4 4H9a4 4 0 01-4-4v-8a1 1 0 011-1z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Supprimer</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</section>
|
||||
|
||||
<!-- Watch History Section -->
|
||||
@@ -67,58 +179,154 @@
|
||||
</svg>
|
||||
Historique de visionnage
|
||||
</h3>
|
||||
<span class="text-xs bg-slate-700 text-slate-300 px-2 py-1 rounded-full">
|
||||
{{ watchHistory().length }} vidéo(s)
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs bg-slate-700 text-slate-300 px-2 py-1 rounded-full">
|
||||
{{ watchHistory().length }} vidéo(s)
|
||||
<span *ngIf="searchQuery()">
|
||||
• {{ filteredWatchHistory().length }} résultat(s)
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="watchHistory().length === 0" class="text-slate-400 text-center py-4">
|
||||
Aucune vidéo récemment regardée
|
||||
</div>
|
||||
<ng-container *ngIf="searchQuery(); else normalWatchHistory">
|
||||
<!-- Mode recherche -->
|
||||
<div *ngIf="isLoading()" class="text-center py-6">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
|
||||
<p class="mt-2 text-slate-400">Recherche en cours...</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isLoading() && filteredWatchHistory().length === 0" class="text-slate-400 text-center py-6">
|
||||
Aucun résultat trouvé dans l'historique de visionnage pour "{{ searchQuery() }}"
|
||||
</div>
|
||||
|
||||
<ul *ngIf="!isLoading() && filteredWatchHistory().length > 0" class="space-y-3">
|
||||
<li *ngFor="let w of filteredWatchHistory()"
|
||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/watch', w.video_id]" [queryParams]="{ p: w.provider }"
|
||||
class="block p-4 pr-16 hover:no-underline">
|
||||
<div class="flex">
|
||||
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-700 rounded overflow-hidden flex items-center justify-center">
|
||||
@if (w.thumbnail) {
|
||||
<img [src]="w.thumbnail" alt="" class="w-full h-full object-cover" />
|
||||
} @else {
|
||||
<div class="text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-slate-600">
|
||||
<div class="h-full bg-red-500" [style.width.%]="getProgressPercentage(w)"></div>
|
||||
</div>
|
||||
<div *ngIf="w.duration_seconds" class="absolute bottom-1 right-1 bg-black/80 text-white text-2xs px-1 rounded">
|
||||
{{ formatDuration(w.duration_seconds) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-1 min-w-0">
|
||||
<h4 class="flex items-center gap-2 text-sm font-medium text-slate-100 group-hover:text-blue-400 transition-colors truncate">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span class="truncate">{{ w.title || 'Sans titre' }}</span>
|
||||
</h4>
|
||||
<div class="mt-1 text-xs text-slate-400">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
||||
[ngStyle]="getProviderColors(w.provider)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
{{ getProviderDisplayName(w.provider) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Visionné le {{ formatDate(w.watched_at) }}
|
||||
</p>
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
<span>Progression: {{ formatDuration(w.progress_seconds || 0) }} / {{ formatDuration(w.duration_seconds || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
<button (click)="onDeleteWatch(w); $event.preventDefault(); $event.stopPropagation();"
|
||||
[disabled]="deletingWatchId() === w.id"
|
||||
class="absolute bottom-2 right-2 inline-flex items-center gap-1 px-2 py-1 rounded-md bg-red-600/90 hover:bg-red-500/90 text-white text-xs disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-3 w-3">
|
||||
<path d="M9 3a1 1 0 00-1 1v1H5a1 1 0 100 2h14a1 1 0 100-2h-3V4a1 1 0 00-1-1H9zM6 9a1 1 0 011 1v8a2 2 0 002 2h6a2 2 0 002-2v-8a1 1 0 112 0v8a4 4 0 01-4 4H9a4 4 0 01-4-4v-8a1 1 0 011-1z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Supprimer</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ul class="space-y-3">
|
||||
<li *ngFor="let w of watchHistory()"
|
||||
class="group bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/watch', w.video_id]" [queryParams]="{ p: w.provider }"
|
||||
class="block p-4 hover:no-underline">
|
||||
<div class="flex">
|
||||
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-700 rounded overflow-hidden flex items-center justify-center">
|
||||
<div class="text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<ng-template #normalWatchHistory>
|
||||
<!-- Mode affichage normal -->
|
||||
<div *ngIf="watchHistory().length === 0" class="text-slate-400 text-center py-4">
|
||||
Aucune vidéo récemment regardée
|
||||
</div>
|
||||
|
||||
<ul *ngIf="watchHistory().length > 0" class="space-y-3">
|
||||
<li *ngFor="let w of watchHistory()"
|
||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/watch', w.video_id]" [queryParams]="{ p: w.provider }"
|
||||
class="block p-4 pr-16 hover:no-underline">
|
||||
<div class="flex">
|
||||
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-700 rounded overflow-hidden flex items-center justify-center">
|
||||
@if (w.thumbnail) {
|
||||
<img [src]="w.thumbnail" alt="" class="w-full h-full object-cover" />
|
||||
} @else {
|
||||
<div class="text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-slate-600">
|
||||
<div class="h-full bg-red-500" [style.width.%]="getProgressPercentage(w)"></div>
|
||||
</div>
|
||||
<div *ngIf="w.duration_seconds" class="absolute bottom-1 right-1 bg-black/80 text-white text-2xs px-1 rounded">
|
||||
{{ formatDuration(w.duration_seconds) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-slate-600">
|
||||
<div class="h-full bg-red-500" [style.width.%]="getProgressPercentage(w)"></div>
|
||||
</div>
|
||||
<div *ngIf="w.duration_seconds" class="absolute bottom-1 right-1 bg-black/80 text-white text-2xs px-1 rounded">
|
||||
{{ formatDuration(w.duration_seconds) }}
|
||||
<div class="ml-4 flex-1 min-w-0">
|
||||
<h4 class="flex items-center gap-2 text-sm font-medium text-slate-100 group-hover:text-blue-400 transition-colors truncate">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span class="truncate">{{ w.title || 'Sans titre' }}</span>
|
||||
</h4>
|
||||
<div class="mt-1 text-xs text-slate-400">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
||||
[ngStyle]="getProviderColors(w.provider)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
{{ getProviderDisplayName(w.provider) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Visionné le {{ formatDate(w.watched_at) }}
|
||||
</p>
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
<span>Progression: {{ formatDuration(w.progress_seconds || 0) }} / {{ formatDuration(w.duration_seconds || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="ml-4 flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-slate-100 group-hover:text-blue-400 transition-colors truncate">
|
||||
{{ w.title || 'Sans titre' }}
|
||||
</h4>
|
||||
<div class="mt-1 text-xs text-slate-400">
|
||||
<span class="inline-flex items-center bg-slate-800/80 px-2 py-0.5 rounded mr-2">
|
||||
{{ w.provider }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-2xs text-slate-500">
|
||||
<span>Regardé le {{ formatDate(w.watched_at) }}</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span>Progression: {{ formatDuration(w.progress_seconds || 0) }} / {{ formatDuration(w.duration_seconds || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="text-slate-400 hover:text-blue-400 p-1 -mr-1 self-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
<button (click)="onDeleteWatch(w); $event.preventDefault(); $event.stopPropagation();"
|
||||
[disabled]="deletingWatchId() === w.id"
|
||||
class="absolute bottom-2 right-2 inline-flex items-center gap-1 px-2 py-1 rounded-md bg-red-600/90 hover:bg-red-500/90 text-white text-xs disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-3 w-3">
|
||||
<path d="M9 3a1 1 0 00-1 1v1H5a1 1 0 100 2h14a1 1 0 100-2h-3V4a1 1 0 00-1-1H9zM6 9a1 1 0 011 1v8a2 2 0 002 2h6a2 2 0 002-2v-8a1 1 0 112 0v8a4 4 0 01-4 4H9a4 4 0 01-4-4v-8a1 1 0 011-1z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Supprimer</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal, computed, effect } from '@angular/core';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { HistoryService, SearchHistoryItem, WatchHistoryItem } from '../../../services/history.service';
|
||||
|
||||
@Component({
|
||||
@@ -8,7 +12,7 @@ import { HistoryService, SearchHistoryItem, WatchHistoryItem } from '../../../se
|
||||
standalone: true,
|
||||
templateUrl: './history.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, RouterModule]
|
||||
imports: [CommonModule, RouterModule, FormsModule, NgClass]
|
||||
})
|
||||
export class HistoryComponent {
|
||||
private history = inject(HistoryService);
|
||||
@@ -16,22 +20,122 @@ export class HistoryComponent {
|
||||
loading = signal<boolean>(false);
|
||||
searchHistory = signal<SearchHistoryItem[]>([]);
|
||||
watchHistory = signal<WatchHistoryItem[]>([]);
|
||||
|
||||
searchQuery = signal<string>('');
|
||||
isLoading = signal<boolean>(false);
|
||||
deletingSearchId = signal<string | null>(null);
|
||||
deletingWatchId = signal<string | null>(null);
|
||||
|
||||
// Historiques complets
|
||||
filteredSearchHistory = signal<SearchHistoryItem[]>([]);
|
||||
filteredWatchHistory = signal<WatchHistoryItem[]>([]);
|
||||
|
||||
// Sujet pour la recherche avec debounce
|
||||
private searchTerms = new Subject<string>();
|
||||
|
||||
constructor() {
|
||||
// Chargement initial des données
|
||||
this.reload();
|
||||
|
||||
// Configuration de la recherche avec debounce
|
||||
this.searchTerms.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
switchMap((term: string) => {
|
||||
if (!term.trim()) {
|
||||
// Si la recherche est vide, on recharge les 50 premiers éléments
|
||||
return of({ search: [], watch: [] });
|
||||
}
|
||||
this.isLoading.set(true);
|
||||
return this.history.searchInSearchHistory(term).pipe(
|
||||
switchMap(searchResults =>
|
||||
this.history.searchInWatchHistory(term).pipe(
|
||||
switchMap(watchResults =>
|
||||
of({ search: searchResults, watch: watchResults })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
).subscribe({
|
||||
next: ({ search, watch }) => {
|
||||
this.filteredSearchHistory.set(search);
|
||||
this.filteredWatchHistory.set(watch);
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Réagir aux changements de la requête de recherche
|
||||
effect(() => {
|
||||
const query = this.searchQuery();
|
||||
if (query === '') {
|
||||
// Si la recherche est vide, on recharge les 50 premiers éléments
|
||||
this.reload();
|
||||
} else {
|
||||
// Sinon on lance la recherche
|
||||
this.searchTerms.next(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loading.set(true);
|
||||
this.isLoading.set(true);
|
||||
this.history.getSearchHistory(50).subscribe({
|
||||
next: (items) => this.searchHistory.set(items || []),
|
||||
error: () => {},
|
||||
next: (items) => {
|
||||
this.searchHistory.set(items || []);
|
||||
if (!this.searchQuery()) {
|
||||
this.filteredSearchHistory.set(items || []);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
});
|
||||
this.history.getWatchHistory(50).subscribe({
|
||||
next: (items) => this.watchHistory.set(items || []),
|
||||
next: (items) => {
|
||||
this.watchHistory.set(items || []);
|
||||
if (!this.searchQuery()) {
|
||||
this.filteredWatchHistory.set(items || []);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
complete: () => {
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Delete a single search history item
|
||||
onDeleteSearch(item: SearchHistoryItem): void {
|
||||
if (!item?.id || this.deletingSearchId()) return;
|
||||
this.deletingSearchId.set(item.id);
|
||||
this.history.deleteSearchItem(item.id).subscribe({
|
||||
next: () => {
|
||||
// Remove from both full and filtered lists
|
||||
this.searchHistory.update(list => (list || []).filter(x => x.id !== item.id));
|
||||
this.filteredSearchHistory.update(list => (list || []).filter(x => x.id !== item.id));
|
||||
},
|
||||
error: () => {},
|
||||
complete: () => this.deletingSearchId.set(null)
|
||||
});
|
||||
}
|
||||
|
||||
// --- Delete a single watch history item
|
||||
onDeleteWatch(item: WatchHistoryItem): void {
|
||||
if (!item?.id || this.deletingWatchId()) return;
|
||||
this.deletingWatchId.set(item.id);
|
||||
this.history.deleteWatchItem(item.id).subscribe({
|
||||
next: () => {
|
||||
this.watchHistory.update(list => (list || []).filter(x => x.id !== item.id));
|
||||
this.filteredWatchHistory.update(list => (list || []).filter(x => x.id !== item.id));
|
||||
},
|
||||
error: () => {},
|
||||
complete: () => this.deletingWatchId.set(null)
|
||||
});
|
||||
this.loading.set(false);
|
||||
}
|
||||
|
||||
// Format a date to a readable string
|
||||
@@ -55,7 +159,7 @@ export class HistoryComponent {
|
||||
const filters = JSON.parse(item.filters_json);
|
||||
if (filters.provider) {
|
||||
const providerName = this.formatProviderName(filters.provider);
|
||||
return { name: providerName, id: filters.provider };
|
||||
return { name: providerName, id: String(filters.provider).toLowerCase() };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing filters_json:', e);
|
||||
@@ -68,7 +172,7 @@ export class HistoryComponent {
|
||||
const provider = url.searchParams.get('provider');
|
||||
if (provider) {
|
||||
const providerName = this.formatProviderName(provider);
|
||||
return { name: providerName, id: provider };
|
||||
return { name: providerName, id: provider.toLowerCase() };
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a valid URL or no provider in query params
|
||||
@@ -77,7 +181,7 @@ export class HistoryComponent {
|
||||
// Default to the selected provider if available
|
||||
const defaultProvider = this.getDefaultProvider();
|
||||
if (defaultProvider) {
|
||||
return { name: this.formatProviderName(defaultProvider), id: defaultProvider };
|
||||
return { name: this.formatProviderName(defaultProvider), id: defaultProvider.toLowerCase() };
|
||||
}
|
||||
|
||||
// Fallback to 'all' if no provider can be determined
|
||||
@@ -90,11 +194,11 @@ export class HistoryComponent {
|
||||
// Try to get from current URL
|
||||
const url = new URL(window.location.href);
|
||||
const provider = url.searchParams.get('provider');
|
||||
if (provider) return provider;
|
||||
if (provider) return provider.toLowerCase();
|
||||
|
||||
// Or get from localStorage if available
|
||||
const savedProvider = localStorage.getItem('selectedProvider');
|
||||
if (savedProvider) return savedProvider;
|
||||
if (savedProvider) return savedProvider.toLowerCase();
|
||||
|
||||
// Default to youtube if nothing else
|
||||
return 'youtube';
|
||||
@@ -115,6 +219,35 @@ export class HistoryComponent {
|
||||
return providerMap[providerId.toLowerCase()] || providerId;
|
||||
}
|
||||
|
||||
// Expose a public-friendly name for provider IDs for templates
|
||||
getProviderDisplayName(providerId: string | null | undefined): string {
|
||||
if (!providerId) return '';
|
||||
return this.formatProviderName(providerId);
|
||||
}
|
||||
|
||||
// Brand colors for provider badges (inline to avoid Tailwind purge issues)
|
||||
getProviderColors(providerId: string): { [key: string]: string } {
|
||||
const id = (providerId || '').toLowerCase();
|
||||
switch (id) {
|
||||
case 'youtube':
|
||||
return { backgroundColor: 'rgba(220, 38, 38, 0.15)', color: 'rgb(248, 113, 113)', borderColor: 'rgba(239, 68, 68, 0.3)' };
|
||||
case 'vimeo':
|
||||
return { backgroundColor: 'rgba(2, 132, 199, 0.15)', color: 'rgb(125, 211, 252)', borderColor: 'rgba(14, 165, 233, 0.3)' };
|
||||
case 'dailymotion':
|
||||
return { backgroundColor: 'rgba(37, 99, 235, 0.15)', color: 'rgb(147, 197, 253)', borderColor: 'rgba(59, 130, 246, 0.3)' };
|
||||
case 'peertube':
|
||||
return { backgroundColor: 'rgba(245, 158, 11, 0.15)', color: 'rgb(252, 211, 77)', borderColor: 'rgba(245, 158, 11, 0.3)' };
|
||||
case 'rumble':
|
||||
return { backgroundColor: 'rgba(22, 163, 74, 0.15)', color: 'rgb(134, 239, 172)', borderColor: 'rgba(34, 197, 94, 0.3)' };
|
||||
case 'twitch':
|
||||
return { backgroundColor: 'rgba(168, 85, 247, 0.15)', color: 'rgb(216, 180, 254)', borderColor: 'rgba(168, 85, 247, 0.3)' };
|
||||
case 'odysee':
|
||||
return { backgroundColor: 'rgba(236, 72, 153, 0.15)', color: 'rgb(251, 207, 232)', borderColor: 'rgba(236, 72, 153, 0.3)' };
|
||||
default:
|
||||
return { backgroundColor: 'rgba(30, 41, 59, 0.8)', color: 'rgb(203, 213, 225)', borderColor: 'rgb(51, 65, 85)' };
|
||||
}
|
||||
}
|
||||
|
||||
// Format seconds to HH:MM:SS or MM:SS
|
||||
formatDuration(seconds: number): string {
|
||||
if (!seconds && seconds !== 0) return '00:00';
|
||||
|
||||
Reference in New Issue
Block a user