fix(search): loading par frame sur les cartes, plus de cadres vides
CI / build-and-test (push) Successful in 14m8s
CI / build-and-test (push) Successful in 14m8s
- video-card: etat de chargement de la miniature (shimmer + spinner), fallback 'Apertu indisponible' avec retry, referrerpolicy no-referrer - grid: eager sur les premieres cartes, bandeau de rafraichissement quand la grille est deja peuplee - search: garde MIN_QUERY_LENGTH=2 (le loading partait pour une requete que request$ filtrait, grille figee sur des skeletons) + filet de securite 20s - skeleton: ligne chaine pour coller a la hauteur reelle de la carte
This commit is contained in:
@@ -22,6 +22,11 @@ import { UserService, type UserPreferences } from '../../services/user.service';
|
||||
import { PROVIDERS, type ProviderId } from '../../app/core/providers/provider-registry';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
/** Longueur minimale d'une requete (aligne sur `SearchService.request$` et le backend). */
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
/** Filet de securite : le loading ne doit jamais rester bloque indefiniment. */
|
||||
const LOADING_TIMEOUT_MS = 20_000;
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
standalone: true,
|
||||
@@ -69,6 +74,8 @@ export class SearchComponent {
|
||||
private lastRecordedKey: string | null = null;
|
||||
// Signature to detect new searches (q + providers + sort); excludes page
|
||||
private lastSearchSignature: string | null = null;
|
||||
// Relache le loading si l'emetteur `request$` ne repond pas (filet de securite).
|
||||
private loadingWatchdog: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
hasQuery = computed(() => this.q().length > 0);
|
||||
providerLabel = computed(() => {
|
||||
@@ -291,6 +298,7 @@ export class SearchComponent {
|
||||
}
|
||||
this.loading.set(false);
|
||||
this.loadingMore.set(false);
|
||||
this.clearLoadingWatchdog();
|
||||
this.error.set(null);
|
||||
// Surface per-provider backend failures (e.g. YouTube quota) so
|
||||
// users understand why only some providers return results.
|
||||
@@ -300,6 +308,7 @@ export class SearchComponent {
|
||||
this.groups.set({} as any);
|
||||
this.loading.set(false);
|
||||
this.loadingMore.set(false);
|
||||
this.clearLoadingWatchdog();
|
||||
this.error.set('Le service de recherche est temporairement indisponible. Réessayer.');
|
||||
}
|
||||
});
|
||||
@@ -340,7 +349,7 @@ export class SearchComponent {
|
||||
this.typeFilter.set(filters.type);
|
||||
this.dateFilter.set(filters.period);
|
||||
|
||||
if (q) {
|
||||
if (q.length >= MIN_QUERY_LENGTH) {
|
||||
this.notice.set(null);
|
||||
// Reset the language filter on a new query (the other filters are URL
|
||||
// driven and must survive a query change).
|
||||
@@ -385,15 +394,18 @@ export class SearchComponent {
|
||||
this.endReached.set(false);
|
||||
this.loadingMore.set(false);
|
||||
this.loading.set(true);
|
||||
this.armLoadingWatchdog();
|
||||
// New search (or sort/filter reset): results restart, scroll to top.
|
||||
// Continuation pages must NOT touch scroll (handled without router nav).
|
||||
try { window.scrollTo(0, 0); } catch {}
|
||||
} else if (!this.groups() || Object.keys(this.groups()).length === 0) {
|
||||
this.loading.set(true);
|
||||
this.armLoadingWatchdog();
|
||||
}
|
||||
this.error.set(null);
|
||||
} else {
|
||||
this.loading.set(false);
|
||||
this.clearLoadingWatchdog();
|
||||
this.groups.set({} as any);
|
||||
this.showUnified.set(false);
|
||||
this.error.set(null);
|
||||
@@ -403,6 +415,36 @@ export class SearchComponent {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Arme le filet de securite du loading. Si `request$` ne repond pas (requete
|
||||
* supprimee par un `switchMap`, cache incoherent, etc.) on stoppe le loading au
|
||||
* lieu de laisser une grille figee sur des skeletons.
|
||||
*/
|
||||
private armLoadingWatchdog(): void {
|
||||
this.clearLoadingWatchdog();
|
||||
this.loadingWatchdog = setTimeout(() => {
|
||||
this.loadingWatchdog = null;
|
||||
if (this.loading()) {
|
||||
this.loading.set(false);
|
||||
this.loadingMore.set(false);
|
||||
if (Object.keys(this.groups() || {}).length === 0) {
|
||||
this.error.set('La recherche met trop de temps a repondre. Reessayez.');
|
||||
}
|
||||
}
|
||||
}, LOADING_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private clearLoadingWatchdog(): void {
|
||||
if (this.loadingWatchdog != null) {
|
||||
clearTimeout(this.loadingWatchdog);
|
||||
this.loadingWatchdog = null;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.clearLoadingWatchdog();
|
||||
}
|
||||
|
||||
// Lectures de métadonnées déléguées au modèle de filtres partagé
|
||||
// (server/search-filters.mjs côté API, src/app/search/filters.ts côté front :
|
||||
// même implémentation = mêmes résultats).
|
||||
@@ -735,6 +777,7 @@ export class SearchComponent {
|
||||
const next = (this.pageParam() || 1) + 1;
|
||||
this.pageParam.set(next);
|
||||
this.loadingMore.set(true);
|
||||
this.armLoadingWatchdog();
|
||||
this.unified.setPage(next);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user