chore: update TypeScript build info cache
This commit is contained in:
@@ -9,16 +9,6 @@ import { Video } from '../../models/video.model';
|
||||
import { TranslatePipe } from '../../pipes/translate.pipe';
|
||||
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
|
||||
|
||||
interface SectionState {
|
||||
key: 'recorded' | 'videos' | 'categories';
|
||||
titleKey: string;
|
||||
items: Video[];
|
||||
nextCursor: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
visibleCount: number; // how many items are currently shown
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-provider-theme',
|
||||
standalone: true,
|
||||
@@ -37,27 +27,25 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
provider = signal<Provider>('youtube');
|
||||
theme = signal<string>('trending');
|
||||
|
||||
// Guard to ignore stale async responses after route/theme/provider changes
|
||||
// Guard to ignore stale async responses
|
||||
private version = 0;
|
||||
|
||||
// Filters state
|
||||
sort = signal<'recent' | 'viewed' | 'longest'>('recent');
|
||||
duration = signal<'any' | 'short' | 'medium' | 'long'>('any');
|
||||
type = signal<'any' | 'video' | 'live' | 'recorded' | 'short'>('any');
|
||||
language = signal<'any' | 'en' | 'fr'>('any');
|
||||
date = signal<'any' | 'day' | 'week' | 'month' | 'year'>('any');
|
||||
|
||||
// UI
|
||||
// Data state
|
||||
allVideos = signal<Video[]>([]);
|
||||
nextCursor = signal<string | null>(null);
|
||||
loading = signal(false);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
// Derived state for the UI
|
||||
providersList = computed(() => this.instances.providers());
|
||||
categories = computed(() => this.themes.categoriesFor(this.theme()));
|
||||
|
||||
// Sections (Live removed on provider pages)
|
||||
recorded: SectionState = { key: 'recorded', titleKey: 'themes.recorded', items: [], nextCursor: null, loading: false, error: null, visibleCount: 20 };
|
||||
videos: SectionState = { key: 'videos', titleKey: 'themes.videos', items: [], nextCursor: null, loading: false, error: null, visibleCount: 12 };
|
||||
|
||||
// Derived lists for UI separation
|
||||
shorts: Video[] = []; // aggregated from both sections
|
||||
nonShorts: Video[] = []; // from videos section only
|
||||
recordedNonShorts: Video[] = []; // from recorded section
|
||||
videos = computed(() => this.applyFilters(this.allVideos()));
|
||||
|
||||
constructor() {
|
||||
this.route.paramMap.subscribe(pm => {
|
||||
@@ -65,424 +53,150 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
const theme = pm.get('theme') || 'trending';
|
||||
this.provider.set(provider);
|
||||
this.theme.set(theme);
|
||||
// Keep global provider in sync so header search uses the current context
|
||||
try { this.instances.setSelectedProvider(provider); } catch {}
|
||||
this.resetAll();
|
||||
// Increment version so in-flight requests from previous state are ignored
|
||||
this.version++;
|
||||
this.loadInitial();
|
||||
this.cdr.markForCheck();
|
||||
this.instances.setSelectedProvider(provider);
|
||||
this.resetAndLoad();
|
||||
});
|
||||
|
||||
// Prefetch recalculation on resize (affects columns/rows thresholds)
|
||||
this.handleResize = () => {
|
||||
this.checkPrefetch(this.recorded);
|
||||
this.checkPrefetch(this.videos);
|
||||
};
|
||||
try { window.addEventListener('resize', this.handleResize); } catch {}
|
||||
|
||||
// Re-apply filters on change (Live removed) with a tiny debounce to avoid thrashing
|
||||
// Re-apply filters on change
|
||||
effect(() => {
|
||||
const _s = this.sort();
|
||||
const _d = this.duration();
|
||||
const _l = this.language();
|
||||
const _dt = this.date();
|
||||
const apply = () => {
|
||||
// Reset visible window to ensure top results reflect new sort/filter instantly
|
||||
this.recorded.visibleCount = Math.max(20, this.recorded.visibleCount);
|
||||
// Keep videos at 12-increment pagination
|
||||
this.videos.visibleCount = Math.max(12, this.videos.visibleCount);
|
||||
this.recorded.items = this.applyFilters(this.recorded.items);
|
||||
this.videos.items = this.applyFilters(this.videos.items);
|
||||
// Recompute derived partitions
|
||||
this.partitionVideos();
|
||||
// After filtering, re-evaluate if we need to prefetch more
|
||||
this.checkPrefetch(this.recorded);
|
||||
this.checkPrefetch(this.videos);
|
||||
this.cdr.markForCheck();
|
||||
try { this.cdr.detectChanges(); } catch {}
|
||||
};
|
||||
try {
|
||||
clearTimeout((this as any).__fltTimer);
|
||||
} catch {}
|
||||
(this as any).__fltTimer = setTimeout(apply, 150);
|
||||
}, { allowSignalWrites: true });
|
||||
this.sort();
|
||||
this.duration();
|
||||
this.type();
|
||||
this.language();
|
||||
this.date();
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private resetAll() {
|
||||
for (const s of [this.recorded, this.videos]) {
|
||||
s.items = []; s.nextCursor = null; s.loading = false; s.error = null;
|
||||
s.visibleCount = s.key === 'videos' ? 12 : 20;
|
||||
}
|
||||
this.shorts = [];
|
||||
this.nonShorts = [];
|
||||
this.recordedNonShorts = [];
|
||||
this.cdr.markForCheck();
|
||||
private resetAndLoad() {
|
||||
this.version++;
|
||||
this.allVideos.set([]);
|
||||
this.nextCursor.set(null);
|
||||
this.error.set(null);
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
private baseTokens(): string[] {
|
||||
const t = this.theme();
|
||||
if (t === 'trending') return [];
|
||||
if (t === 'live') return ['live'];
|
||||
return this.themes.tokensFor(t);
|
||||
}
|
||||
|
||||
private buildQueryFor(section: SectionState): string {
|
||||
const tokens = [...this.baseTokens()];
|
||||
if (section.key === 'recorded') {
|
||||
const p = this.provider();
|
||||
// Only providers that benefit from a 'replay' search hint
|
||||
if (p === 'youtube' || p === 'twitch') tokens.push('replay');
|
||||
// For other providers, prefer trending (empty query) to avoid bad/no results
|
||||
}
|
||||
private buildQuery(): string {
|
||||
const tokens = this.themes.tokensFor(this.theme());
|
||||
return tokens.join(' ').trim();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.loading()) return;
|
||||
|
||||
const readiness = this.instances.getProviderReadiness(this.provider());
|
||||
if (!readiness.ready) {
|
||||
this.error.set(readiness.reason || 'Provider not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
const snapshotVersion = this.version;
|
||||
const query = this.buildQuery();
|
||||
const provider = this.provider();
|
||||
|
||||
const apiCall = query
|
||||
? this.api.searchVideosPage(query, this.nextCursor(), provider)
|
||||
: this.api.getTrendingPage(this.nextCursor(), provider);
|
||||
|
||||
apiCall.subscribe({
|
||||
next: (res) => {
|
||||
if (snapshotVersion !== this.version) return;
|
||||
|
||||
const existingIds = new Set(this.allVideos().map(v => v.videoId));
|
||||
const newVideos = (res.items || []).filter(v => !existingIds.has(v.videoId));
|
||||
|
||||
this.allVideos.update(current => [...current, ...newVideos]);
|
||||
this.nextCursor.set(res.nextCursor || null);
|
||||
this.loading.set(false);
|
||||
this.cdr.markForCheck();
|
||||
},
|
||||
error: (e) => {
|
||||
if (snapshotVersion !== this.version) return;
|
||||
this.error.set(e.message || 'Failed to load videos');
|
||||
this.loading.set(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private applyFilters(list: Video[]): Video[] {
|
||||
let arr = list.slice();
|
||||
const d = this.duration();
|
||||
if (d !== 'any') {
|
||||
let arr = [...list];
|
||||
const durationFilter = this.duration();
|
||||
if (durationFilter !== 'any') {
|
||||
arr = arr.filter(v => {
|
||||
const dur = v.duration || 0;
|
||||
if (d === 'short') return dur > 0 && dur < 600;
|
||||
if (d === 'medium') return dur >= 600 && dur < 1800;
|
||||
if (d === 'long') return dur >= 1800;
|
||||
if (durationFilter === 'short') return dur > 0 && dur < 240; // < 4 min
|
||||
if (durationFilter === 'medium') return dur >= 240 && dur < 1200; // 4-20 min
|
||||
if (durationFilter === 'long') return dur >= 1200; // > 20 min
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Language heuristic (very lightweight)
|
||||
const lang = this.language();
|
||||
if (lang !== 'any') {
|
||||
const isFrWord = (s: string) => /\b(le|la|les|des|un|une|et|ou|pour|avec|sans|sur|dans|est|cette|vidéo)\b/i.test(s);
|
||||
const isEnWord = (s: string) => /\b(the|a|an|and|or|with|without|for|this|video|live)\b/i.test(s);
|
||||
const typeFilter = this.type();
|
||||
if (typeFilter !== 'any') {
|
||||
arr = arr.filter(v => {
|
||||
const text = `${v.title} ${v.uploaderName}`;
|
||||
const fr = isFrWord(text) || /[éèêàùçîïô]/i.test(text);
|
||||
const en = isEnWord(text);
|
||||
return lang === 'fr' ? fr && !en : en && !fr;
|
||||
if (typeFilter === 'video') return v.type === 'video';
|
||||
if (typeFilter === 'live') return v.type === 'live' || v.type === 'channel';
|
||||
if (typeFilter === 'recorded') return v.type === 'recorded';
|
||||
if (typeFilter === 'short') return v.type === 'short' || v.duration < 60;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Date filter based on uploaded timestamp if available
|
||||
const dateOpt = this.date();
|
||||
if (dateOpt !== 'any') {
|
||||
const langFilter = this.language();
|
||||
if (langFilter !== 'any') {
|
||||
// Simple heuristic, can be improved
|
||||
const isFr = (s: string) => /[éèêàùçîïô]/.test(s) || /\b(le|la|les|un|une)\b/i.test(s);
|
||||
const isEn = (s: string) => /\b(the|a|an|and|for)\b/i.test(s);
|
||||
arr = arr.filter(v => {
|
||||
const text = `${v.title} ${v.uploaderName}`.toLowerCase();
|
||||
if (langFilter === 'fr') return isFr(text);
|
||||
if (langFilter === 'en') return isEn(text) && !isFr(text);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const dateFilter = this.date();
|
||||
if (dateFilter !== 'any') {
|
||||
const now = Date.now();
|
||||
const cutoff = (() => {
|
||||
if (dateOpt === 'day') return now - 24 * 3600_000;
|
||||
if (dateOpt === 'week') return now - 7 * 24 * 3600_000;
|
||||
if (dateOpt === 'month') return now - 30 * 24 * 3600_000;
|
||||
if (dateOpt === 'year') return now - 365 * 24 * 3600_000;
|
||||
return 0;
|
||||
})();
|
||||
const cutoff = {
|
||||
day: now - 24 * 3600 * 1000,
|
||||
week: now - 7 * 24 * 3600 * 1000,
|
||||
month: now - 30 * 24 * 3600 * 1000,
|
||||
year: now - 365 * 24 * 3600 * 1000,
|
||||
}[dateFilter] || 0;
|
||||
if (cutoff > 0) {
|
||||
arr = arr.filter(v => (v.uploaded || 0) >= cutoff);
|
||||
}
|
||||
}
|
||||
const s = this.sort();
|
||||
if (s === 'recent') arr.sort((a, b) => (b.uploaded || 0) - (a.uploaded || 0));
|
||||
if (s === 'viewed') arr.sort((a, b) => (b.views || 0) - (a.views || 0));
|
||||
if (s === 'longest') arr.sort((a, b) => (b.duration || 0) - (a.duration || 0));
|
||||
|
||||
const sortOrder = this.sort();
|
||||
if (sortOrder === 'recent') arr.sort((a, b) => (b.uploaded || 0) - (a.uploaded || 0));
|
||||
if (sortOrder === 'viewed') arr.sort((a, b) => (b.views || 0) - (a.views || 0));
|
||||
if (sortOrder === 'longest') arr.sort((a, b) => (b.duration || 0) - (a.duration || 0));
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Pagination des shorts
|
||||
shortsVisibleCount = 20;
|
||||
|
||||
// Charge plus de shorts
|
||||
showMoreShorts() {
|
||||
this.shortsVisibleCount += 20;
|
||||
// Vérifier si nous avons assez de vidéos pour afficher
|
||||
if (this.shorts.length <= this.shortsVisibleCount) {
|
||||
// Si nous n'avons pas assez de vidéos, essayer d'en charger plus
|
||||
this.checkAndLoadMoreShorts();
|
||||
}
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
// Vérifie et charge plus de shorts si nécessaire
|
||||
private checkAndLoadMoreShorts() {
|
||||
// Vérifier si nous avons déjà chargé toutes les vidéos disponibles
|
||||
const totalShorts = this.videos.items.filter(v => this.isShortItem(v)).length +
|
||||
this.recorded.items.filter(v => this.isShortItem(v)).length;
|
||||
|
||||
// Si nous avons moins de vidéos que ce que nous voulons afficher, essayer d'en charger plus
|
||||
if (totalShorts <= this.shortsVisibleCount) {
|
||||
// Essayer de charger plus de vidéos de la section videos d'abord
|
||||
if (this.videos.nextCursor && !this.videos.loading) {
|
||||
this.loadMore(this.videos);
|
||||
}
|
||||
// Puis essayer de charger plus de vidéos de la section recorded
|
||||
else if (this.recorded.nextCursor && !this.recorded.loading) {
|
||||
this.loadMore(this.recorded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Classification per spec
|
||||
private isShortItem(x: any): boolean {
|
||||
try {
|
||||
if (!x) return false;
|
||||
if (x.type === 'short') return true;
|
||||
if ((x as any).isShort === true) return true;
|
||||
// Platform URL hints (best-effort)
|
||||
const u = String((x as any).url || '');
|
||||
if (u) {
|
||||
// YouTube shorts URLs: /shorts/VIDEOID
|
||||
if (/\/shorts\//i.test(u)) return true;
|
||||
// General reels/clips hints used by some providers
|
||||
if (/\b(reel|reels|clip|clips)\b/i.test(u)) return true;
|
||||
}
|
||||
const dur = Number((x as any).durationSeconds ?? x.duration ?? 0);
|
||||
if (dur > 0 && dur <= 60) return true;
|
||||
const ar = Number((x as any).aspectRatio ?? 0);
|
||||
if (ar > 1.2) return true; // treat vertical as shorts if hinted
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private partitionVideos() {
|
||||
const vids = this.videos.items || [];
|
||||
const recs = this.recorded.items || [];
|
||||
const shortsAgg: Video[] = [];
|
||||
const vidsLongs: Video[] = [];
|
||||
const recsLongs: Video[] = [];
|
||||
const seenShort = new Set<string>();
|
||||
|
||||
const pushShort = (x: Video) => {
|
||||
const k = x.videoId || x.url || '';
|
||||
if (k && !seenShort.has(k)) {
|
||||
seenShort.add(k);
|
||||
shortsAgg.push(x);
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrer les shorts et les vidéos longues pour les vidéos
|
||||
for (const v of vids) {
|
||||
if (this.isShortItem(v)) {
|
||||
pushShort(v);
|
||||
} else {
|
||||
vidsLongs.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer les shorts et les vidéos longues pour les enregistrements
|
||||
for (const r of recs) {
|
||||
if (this.isShortItem(r)) {
|
||||
pushShort(r);
|
||||
} else {
|
||||
recsLongs.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les tableaux de vidéos
|
||||
this.shorts = shortsAgg;
|
||||
|
||||
// Conserver les vidéos déjà affichées si elles existent encore
|
||||
const currentNonShorts = new Set(this.nonShorts.map(v => v.videoId || v.url || ''));
|
||||
const currentRecordedNonShorts = new Set(this.recordedNonShorts.map(v => v.videoId || v.url || ''));
|
||||
|
||||
// Mettre à jour les vidéos non-courtes en conservant l'ordre et les éléments existants
|
||||
this.nonShorts = [
|
||||
...this.nonShorts.filter(v => vidsLongs.some(v2 => (v2.videoId || v2.url) === (v.videoId || v.url))),
|
||||
...vidsLongs.filter(v => !currentNonShorts.has(v.videoId || v.url || ''))
|
||||
].slice(0, this.videos.visibleCount);
|
||||
|
||||
// Mettre à jour les enregistrements non-courts en conservant l'ordre et les éléments existants
|
||||
this.recordedNonShorts = [
|
||||
...this.recordedNonShorts.filter(v => recsLongs.some(v2 => (v2.videoId || v2.url) === (v.videoId || v.url))),
|
||||
...recsLongs.filter(v => !currentRecordedNonShorts.has(v.videoId || v.url || ''))
|
||||
].slice(0, this.recorded.visibleCount);
|
||||
|
||||
// Mettre à jour le compteur de shorts visibles si nécessaire
|
||||
if (this.shortsVisibleCount > this.shorts.length) {
|
||||
this.shortsVisibleCount = this.shorts.length;
|
||||
}
|
||||
|
||||
// Forcer la détection des changements
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
changeProvider(p: any) {
|
||||
const provider = String(p) as Provider;
|
||||
// Sync the global provider so header search reflects the current context immediately
|
||||
try { this.instances.setSelectedProvider(provider); } catch {}
|
||||
this.instances.setSelectedProvider(provider);
|
||||
this.router.navigate(['/p', provider, 't', this.theme()]);
|
||||
}
|
||||
|
||||
onSortChange(val: any) { this.sort.set(val as any); }
|
||||
onDurationChange(val: any) { this.duration.set(val as any); }
|
||||
onTypeChange(val: any) { this.type.set(val as any); }
|
||||
onLanguageChange(val: any) { this.language.set(val as any); }
|
||||
onDateChange(val: any) { this.date.set(val as any); }
|
||||
|
||||
loadInitial() {
|
||||
const readiness = this.instances.getProviderReadiness(this.provider());
|
||||
if (!readiness.ready) {
|
||||
const msg = readiness.reason || 'Provider not ready';
|
||||
console.warn('[ProviderTheme] Provider not ready', { provider: this.provider(), reason: msg, theme: this.theme() });
|
||||
this.recorded.error = msg; this.recorded.loading = false;
|
||||
this.videos.error = msg; this.videos.loading = false;
|
||||
return;
|
||||
}
|
||||
this.loadMore(this.recorded);
|
||||
this.loadMore(this.videos);
|
||||
retry() {
|
||||
this.error.set(null);
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
loadMore(section: SectionState) {
|
||||
if (section.loading) return;
|
||||
const readiness = this.instances.getProviderReadiness(this.provider());
|
||||
if (!readiness.ready) {
|
||||
section.error = readiness.reason || 'Provider not ready';
|
||||
console.warn('[ProviderTheme] Skip load: provider not ready', { provider: this.provider(), section: section.key, reason: section.error, theme: this.theme() });
|
||||
section.loading = false;
|
||||
return;
|
||||
}
|
||||
section.loading = true;
|
||||
this.cdr.markForCheck();
|
||||
const provider = this.provider();
|
||||
|
||||
const query = this.buildQueryFor(section);
|
||||
const snapshotVersion = this.version;
|
||||
|
||||
// If YouTube quota has already been flagged exceeded, surface it and avoid requests
|
||||
if (provider === 'youtube' && this.api.quotaExceeded$.getValue()) {
|
||||
section.error = 'YouTube quota exceeded. Try again later or switch provider.';
|
||||
section.loading = false;
|
||||
console.warn('[ProviderTheme] Skip load: YouTube quota exceeded', { section: section.key, theme: this.theme() });
|
||||
return;
|
||||
}
|
||||
|
||||
const next = (res: { items: Video[]; nextCursor?: string | null }) => {
|
||||
// Ignore responses from a previous state
|
||||
if (snapshotVersion !== this.version) return;
|
||||
// Deduplicate by videoId or URL before applying filters
|
||||
const existing = new Set(section.items.map(v => v.videoId || v.url || ''));
|
||||
const beforeLen = section.items.length;
|
||||
const merged: Video[] = [...section.items];
|
||||
const incoming = res.items || [];
|
||||
const beforeKeys = existing.size;
|
||||
for (const v of res.items || []) {
|
||||
const key = v.videoId || v.url || '';
|
||||
if (!existing.has(key)) { existing.add(key); merged.push(v); }
|
||||
}
|
||||
const afterKeys = existing.size;
|
||||
const added = Math.max(0, afterKeys - beforeKeys);
|
||||
const duplicates = Math.max(0, (incoming.length || 0) - added);
|
||||
|
||||
// Mettre à jour les éléments de la section
|
||||
section.items = this.applyFilters(merged);
|
||||
section.nextCursor = res.nextCursor || null;
|
||||
section.loading = false;
|
||||
|
||||
// Mettre à jour les tableaux de vidéos visibles
|
||||
if (section.key === 'videos') {
|
||||
this.nonShorts = section.items.filter(v => !this.isShortItem(v)).slice(0, section.visibleCount);
|
||||
} else if (section.key === 'recorded') {
|
||||
this.recordedNonShorts = section.items.filter(v => !this.isShortItem(v)).slice(0, section.visibleCount);
|
||||
}
|
||||
|
||||
// Mettre à jour les shorts si nécessaire
|
||||
this.partitionVideos();
|
||||
if (incoming.length === 0) {
|
||||
console.info('[ProviderTheme] Empty page', { provider, section: section.key, theme: this.theme(), query, cursor: section.nextCursor });
|
||||
}
|
||||
if (!res.nextCursor) {
|
||||
console.info('[ProviderTheme] Reached end of pagination', { provider, section: section.key, theme: this.theme(), total: section.items.length });
|
||||
}
|
||||
if (duplicates > 0) {
|
||||
console.debug('[ProviderTheme] Deduplicated incoming items', { provider, section: section.key, duplicates, added, before: beforeLen, after: section.items.length });
|
||||
}
|
||||
// Post-process for YouTube: fetch durations for items lacking them to make duration filters/sorts work
|
||||
if (provider === 'youtube') {
|
||||
try {
|
||||
const wantIds = merged.filter(v => (v.duration || 0) === 0 && !!v.videoId).slice(0, 50).map(v => v.videoId as string);
|
||||
const unique = Array.from(new Set(wantIds));
|
||||
if (unique.length) {
|
||||
this.api.getYouTubeDurations(unique).subscribe({
|
||||
next: (mapDur) => {
|
||||
if (snapshotVersion !== this.version) return;
|
||||
if (!mapDur) return;
|
||||
// Update durations in-place
|
||||
for (const v of section.items) {
|
||||
const id = v.videoId || '';
|
||||
if (id && mapDur[id] != null) v.duration = Number(mapDur[id]) || 0;
|
||||
}
|
||||
// Re-apply filters/sorts that depend on duration
|
||||
section.items = this.applyFilters(section.items);
|
||||
this.partitionVideos();
|
||||
this.checkPrefetch(section);
|
||||
this.cdr.markForCheck();
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
this.partitionVideos();
|
||||
// Logic-based prefetch: if we are within ~2 rows of the end, prefetch next page
|
||||
this.checkPrefetch(section);
|
||||
this.cdr.markForCheck();
|
||||
try { this.cdr.detectChanges(); } catch {}
|
||||
};
|
||||
|
||||
const onErr = (e?: any) => {
|
||||
// Specific message for quota exceeded on YouTube
|
||||
if (provider === 'youtube' && e?.status === 403 && (e?.error?.error?.errors?.[0]?.reason === 'quotaExceeded' || /quota/i.test(String(e?.error?.error?.message || e?.message || '')))) {
|
||||
this.api.quotaExceeded$.next(true);
|
||||
section.error = 'YouTube quota exceeded. Try again later or switch provider.';
|
||||
} else if (provider === 'peertube') {
|
||||
const inst = this.instances.activePeerTubeInstance();
|
||||
const status = e?.status;
|
||||
if (status === 404) section.error = `PeerTube (${inst}) introuvable. Essayez une autre instance.`;
|
||||
else if (status === 429) section.error = `PeerTube (${inst}) limite atteinte. Réessayez plus tard.`;
|
||||
else section.error = `PeerTube (${inst}) indisponible. Réessayez plus tard.`;
|
||||
} else {
|
||||
section.error = 'Erreur de chargement';
|
||||
}
|
||||
section.loading = false;
|
||||
console.error('[ProviderTheme] Load error', { provider, section: section.key, theme: this.theme(), query, cursor: section.nextCursor, error: String(e?.message || e) });
|
||||
this.cdr.markForCheck();
|
||||
try { this.cdr.detectChanges(); } catch {}
|
||||
};
|
||||
|
||||
if (query) {
|
||||
this.api.searchVideosPage(query, section.nextCursor, provider).subscribe({ next, error: onErr });
|
||||
} else {
|
||||
this.api.getTrendingPage(section.nextCursor, provider).subscribe({ next, error: onErr });
|
||||
}
|
||||
}
|
||||
|
||||
// Increase visible items by 20; fetch the next page if we need more data
|
||||
showMore(section: SectionState) {
|
||||
const step = section.key === 'videos' ? 12 : 20;
|
||||
const target = section.visibleCount + step;
|
||||
section.visibleCount = target;
|
||||
|
||||
// Mettre à jour les vidéos visibles immédiatement
|
||||
if (section.key === 'videos') {
|
||||
this.nonShorts = this.videos.items.slice(0, section.visibleCount);
|
||||
} else if (section.key === 'recorded') {
|
||||
this.recordedNonShorts = this.recorded.items.slice(0, section.visibleCount);
|
||||
}
|
||||
|
||||
// Si nous n'avons pas assez d'éléments et qu'il y a une page suivante, la charger
|
||||
const poolLength = this.poolLengthFor(section);
|
||||
if (poolLength < target && section.nextCursor && !section.loading) {
|
||||
console.debug('[ProviderTheme] showMore triggers fetch', { section: section.key, target, have: section.items.length, nextCursor: section.nextCursor });
|
||||
this.loadMore(section);
|
||||
}
|
||||
|
||||
// Vérifier également si nous devons charger plus de contenu
|
||||
this.checkPrefetch(section);
|
||||
this.cdr.markForCheck();
|
||||
|
||||
// Forcer la détection des changements pour mettre à jour l'interface utilisateur
|
||||
try { this.cdr.detectChanges(); } catch {}
|
||||
}
|
||||
|
||||
// Helpers for watch params
|
||||
watchQueryParams(v: Video): Record<string, any> | null {
|
||||
const p = this.provider();
|
||||
const qp: any = { p };
|
||||
@@ -495,47 +209,9 @@ export class ProviderThemePageComponent implements OnDestroy {
|
||||
return qp;
|
||||
}
|
||||
|
||||
// trackBy to reduce DOM churn
|
||||
trackByVideo(_idx: number, v: Video) { return v.videoId || v.url || _idx; }
|
||||
|
||||
// Manual retry helper
|
||||
retry(section: SectionState) {
|
||||
section.error = null;
|
||||
this.loadMore(section);
|
||||
}
|
||||
|
||||
// --- Prefetch helpers ---
|
||||
private handleResize: (() => void) | null = null;
|
||||
|
||||
private currentCols(): number {
|
||||
try {
|
||||
// Tailwind breakpoints: md>=768px -> 4 cols, lg>=1024px -> 6 cols, else 2 cols
|
||||
if (window.matchMedia && window.matchMedia('(min-width: 1024px)').matches) return 6;
|
||||
if (window.matchMedia && window.matchMedia('(min-width: 768px)').matches) return 4;
|
||||
return 2;
|
||||
} catch {
|
||||
return 4; // safe default
|
||||
}
|
||||
}
|
||||
|
||||
private checkPrefetch(section: SectionState) {
|
||||
const cols = this.currentCols();
|
||||
const threshold = cols * 2; // ~2 rows
|
||||
const pool = this.poolLengthFor(section);
|
||||
const remaining = pool - section.visibleCount;
|
||||
if (remaining <= threshold && section.nextCursor && !section.loading) {
|
||||
console.debug('[ProviderTheme] Logic prefetch triggered', { section: section.key, remaining, threshold, nextCursor: section.nextCursor });
|
||||
this.loadMore(section);
|
||||
}
|
||||
}
|
||||
|
||||
private poolLengthFor(section: SectionState): number {
|
||||
if (section.key === 'videos') return this.nonShorts.length || 0;
|
||||
if (section.key === 'recorded') return this.recordedNonShorts.length || 0;
|
||||
return section.items.length;
|
||||
}
|
||||
trackByVideo(_idx: number, v: Video) { return v.videoId || v.url; }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
try { if (this.handleResize) window.removeEventListener('resize', this.handleResize); } catch {}
|
||||
this.version++; // Invalidate any pending requests
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user