feat(preferences): section langues a telecharger (defaut fr+en), transcripts filtres cote serveur et watch
CI / build-and-test (push) Successful in 13m50s

This commit is contained in:
2026-09-26 11:49:56 -04:00
parent 1b0a7eb3ce
commit 655061ba2c
46 changed files with 2018 additions and 380 deletions
+90 -15
View File
@@ -42,15 +42,88 @@
}
@if (hasQuery()) {
<div class="my-4 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
<div class="flex items-center gap-2">
<label class="text-sm text-slate-300" for="sortSelect">Trier par</label>
<select id="sortSelect" class="bg-slate-900 text-slate-100 border border-slate-700 rounded px-2 py-1 text-sm"
[value]="sortParam()" (change)="onSortChange($event)">
<option value="relevance">Pertinence</option>
<option value="date">Date</option>
<option value="views">Vues</option>
</select>
<!-- Pills de type façon directory Twitch : Directs / Vidéos / Clips / Chaînes -->
<div class="flex items-center gap-2 overflow-x-auto pb-1" role="tablist" aria-label="Filtrer par type de contenu">
<button class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'all' ? 'bg-slate-200 text-slate-900 border-slate-200' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'all'" (click)="setTypePill('all')">
Tout ({{ typeCounts().all }})
</button>
<button *ngIf="typeCounts().live > 0 || typePill() === 'live'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'live' ? 'bg-purple-600 text-white border-purple-500' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'live'" (click)="setTypePill('live')">
Directs ({{ typeCounts().live }})
</button>
<button *ngIf="typeCounts().video > 0 || typePill() === 'video'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'video' ? 'bg-slate-200 text-slate-900 border-slate-200' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'video'" (click)="setTypePill('video')">
Vidéos ({{ typeCounts().video }})
</button>
<button *ngIf="typeCounts().clip > 0 || typePill() === 'short'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'short' ? 'bg-red-600 text-white border-red-500' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'short'" (click)="setTypePill('short')">
Clips ({{ typeCounts().clip }})
</button>
<button *ngIf="typeCounts().channel > 0 || typePill() === 'channel'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'channel' ? 'bg-slate-200 text-slate-900 border-slate-200' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'channel'" (click)="setTypePill('channel')">
Chaînes ({{ typeCounts().channel }})
</button>
</div>
<!-- Barre de filtrage (même UX que la page thème / fournisseur) -->
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700 my-4">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-4">
<div>
<label for="sortSelect" class="block text-sm font-medium text-slate-300 mb-1">{{ 'filter.sort' | t }}</label>
<select id="sortSelect" class="w-full bg-slate-700 text-white rounded-md px-3 py-2 border border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
[value]="sortParam()" (change)="onSortChange($event)">
<option value="relevance">{{ 'filter.sort.relevance' | t }}</option>
<option value="date">{{ 'filter.sort.date' | t }}</option>
<option value="views">{{ 'filter.sort.views' | t }}</option>
</select>
</div>
<div>
<label for="duration-filter" class="block text-sm font-medium text-slate-300 mb-1">{{ 'filter.duration' | t }}</label>
<select id="duration-filter" class="w-full bg-slate-700 text-white rounded-md px-3 py-2 border border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
[value]="durationFilter()" (change)="onDurationChange($event)">
<option value="any">{{ 'filter.duration.any' | t }}</option>
<option value="short">{{ 'filter.duration.short' | t }}</option>
<option value="medium">{{ 'filter.duration.medium' | t }}</option>
<option value="long">{{ 'filter.duration.long' | t }}</option>
</select>
</div>
<div>
<label for="type-filter" class="block text-sm font-medium text-slate-300 mb-1">{{ 'filter.type' | t }}</label>
<select id="type-filter" class="w-full bg-slate-700 text-white rounded-md px-3 py-2 border border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
[value]="typeFilter()" (change)="onTypeChange($event)">
<option value="any">{{ 'filter.type.any' | t }}</option>
<option value="video">{{ 'filter.type.video' | t }}</option>
<option value="live">{{ 'filter.type.live' | t }}</option>
<option value="recorded">{{ 'filter.type.recorded' | t }}</option>
<option value="short">{{ 'filter.type.short' | t }}</option>
<option value="channel">{{ 'filter.type.channel' | t }}</option>
</select>
</div>
<div>
<label for="language-filter" class="block text-sm font-medium text-slate-300 mb-1">{{ 'filter.language' | t }}</label>
<select id="language-filter" class="w-full bg-slate-700 text-white rounded-md px-3 py-2 border border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
[value]="languageFilter()" (change)="onLanguageChange($event)">
<option value="any">{{ 'filter.language.any' | t }}</option>
<option value="en">{{ 'filter.language.en' | t }}</option>
<option value="fr">{{ 'filter.language.fr' | t }}</option>
</select>
</div>
<div>
<label for="date-filter" class="block text-sm font-medium text-slate-300 mb-1">{{ 'filter.date' | t }}</label>
<select id="date-filter" class="w-full bg-slate-700 text-white rounded-md px-3 py-2 border border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
[value]="dateFilter()" (change)="onDateChange($event)">
<option value="any">{{ 'filter.date.any' | t }}</option>
<option value="day">{{ 'filter.date.day' | t }}</option>
<option value="week">{{ 'filter.date.week' | t }}</option>
<option value="month">{{ 'filter.date.month' | t }}</option>
<option value="year">{{ 'filter.date.year' | t }}</option>
</select>
</div>
</div>
</div>
@@ -61,17 +134,19 @@
</div>
}
<app-search-result-grid [videos]="filteredResults()" [loading]="loading()"></app-search-result-grid>
<!-- Infinite scroll anchor & indicators -->
<app-search-result-grid [videos]="filteredResults()" [loading]="loading()" [loadingMore]="loadingMore()"></app-search-result-grid>
<!-- Standard infinite scroll: shared sentinel + bottom loader + end state -->
<div class="flex flex-col items-center justify-center py-6">
@if (loading()) {
@if (loading() || loadingMore()) {
<div class="text-slate-400 text-sm">Chargement…</div>
}
@if (endReached()) {
@if (!loading() && !loadingMore() && endReached() && filteredResults().length > 0) {
<div class="text-slate-500 text-xs">Fin des résultats</div>
}
</div>
<div #infiniteScrollAnchor class="h-1 w-full"></div>
<app-infinite-anchor [disabled]="endReached() || !hasQuery()"
[busy]="loading() || loadingMore()"
(loadMore)="loadNextPage()"></app-infinite-anchor>
}
</div>