chore: update Angular cache and TypeScript build info

This commit is contained in:
2025-09-23 22:45:57 -04:00
parent 9a3983a253
commit fe35340948
15 changed files with 234 additions and 77 deletions
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
@@ -0,0 +1,80 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { NgClass, NgIf } from '@angular/common';
export type ProviderKey = 'youtube' | 'dailymotion' | 'twitch' | 'peertube' | 'rumble' | 'odysee';
type ProviderPreset = { label: string; classes: string };
const PROVIDER_PRESETS: Record<ProviderKey, ProviderPreset> = {
youtube: {
label: 'YouTube',
classes: 'bg-gradient-to-r from-red-600/90 to-red-500/90 text-red-50 border border-red-400/60'
},
dailymotion: {
label: 'Dailymotion',
classes: 'bg-gradient-to-r from-sky-600/90 to-sky-500/90 text-sky-50 border border-sky-400/60'
},
twitch: {
label: 'Twitch',
classes: 'bg-gradient-to-r from-violet-600/90 to-purple-500/90 text-violet-50 border border-purple-400/60'
},
peertube: {
label: 'PeerTube',
classes: 'bg-gradient-to-r from-amber-500/90 to-yellow-400/90 text-amber-50 border border-amber-300/60'
},
rumble: {
label: 'Rumble',
classes: 'bg-gradient-to-r from-emerald-600/90 to-green-500/90 text-emerald-50 border border-emerald-400/60'
},
odysee: {
label: 'Odysee',
classes: 'bg-gradient-to-r from-pink-500/90 to-fuchsia-500/90 text-pink-50 border border-pink-300/60'
}
};
@Component({
selector: 'app-provider-badge',
standalone: true,
imports: [NgIf, NgClass],
template: `
<span *ngIf="badgeLabel"
class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-semibold shadow-sm backdrop-blur-sm"
[ngClass]="badgeClasses"
[attr.aria-label]="badgeLabel">
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M4 4a2 2 0 012-2h8a2 2 0 012 2v14h-3v-4H7v4H4V4zm5 12h2v2H9v-2z" />
</svg>
<span>{{ badgeLabel }}</span>
</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProviderBadgeComponent {
@Input() provider: string | null | undefined;
@Input() label: string | null | undefined;
private normalizeProvider(): ProviderKey | null {
if (!this.provider) return null;
const key = this.provider.toLowerCase() as ProviderKey;
return (key in PROVIDER_PRESETS) ? key : null;
}
get badgeLabel(): string | null {
const presetKey = this.normalizeProvider();
if (presetKey) {
return PROVIDER_PRESETS[presetKey].label;
}
if (this.label && this.label.trim()) {
return this.label.trim();
}
return null;
}
get badgeClasses(): string {
const presetKey = this.normalizeProvider();
if (presetKey) {
return PROVIDER_PRESETS[presetKey].classes;
}
return 'bg-slate-800/80 text-slate-100 border border-slate-700';
}
}
@@ -1,6 +1,9 @@
<a [routerLink]="['/watch', video.id]" [queryParams]="buildQueryParams(video)" [state]="{ video }" class="group flex flex-col gap-2">
<div class="relative aspect-video w-full overflow-hidden rounded-lg bg-zinc-800">
<img [src]="video.thumbnailUrl" [alt]="video.title" class="h-full w-full object-cover transition-transform group-hover:scale-105" />
<div class="absolute top-1 right-1">
<app-provider-badge [provider]="video.provider"></app-provider-badge>
</div>
<span *ngIf="video.durationSec !== undefined" class="absolute bottom-1 right-1 rounded bg-black/80 px-1.5 py-0.5 text-xs text-white">
{{ video.durationSec | duration }}
</span>
@@ -3,13 +3,14 @@ import { VideoItem } from '../../models/video-item.model';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { DurationPipe } from '../../pipes/duration.pipe';
import { ProviderBadgeComponent } from '../provider-badge/provider-badge.component';
@Component({
selector: 'app-video-card',
templateUrl: './video-card.component.html',
styleUrls: ['./video-card.component.scss'],
standalone: true,
imports: [CommonModule, RouterLink, DurationPipe],
imports: [CommonModule, RouterLink, DurationPipe, ProviderBadgeComponent],
})
export class VideoCardComponent {
@Input() video!: VideoItem;
+6 -3
View File
@@ -24,11 +24,14 @@
<a [routerLink]="['/watch', video.videoId]" [queryParams]="watchQueryParams(video)" [state]="{ video }" class="group flex flex-col bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1">
<div class="relative">
<img [src]="video.thumbnail" [alt]="video.title" class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded">
{{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }}
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="video.provider || instances.selectedProvider()"></app-provider-badge>
</div>
<div *ngIf="video.duration && video.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded">
{{ video.duration | duration }}
</div>
<div class="absolute bottom-2 left-2">
<app-like-button [videoId]="video.videoId" [provider]="instances.selectedProvider()" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
<app-like-button [videoId]="video.videoId" [provider]="instances.selectedProvider()" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
</div>
</div>
<div class="p-4 flex-grow flex flex-col">
+5 -1
View File
@@ -9,6 +9,8 @@ import { InfiniteAnchorComponent } from '../shared/infinite-anchor/infinite-anch
import { formatRelativeFr } from '../../utils/date.util';
import { TranslatePipe } from '../../pipes/translate.pipe';
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
@Component({
selector: 'app-home',
@@ -20,7 +22,9 @@ import { LikeButtonComponent } from '../shared/components/like-button/like-butto
RouterLink,
InfiniteAnchorComponent,
TranslatePipe,
LikeButtonComponent
LikeButtonComponent,
ProviderBadgeComponent,
DurationPipe
]
})
export class HomeComponent {
@@ -33,7 +33,6 @@
<!-- Actions -->
<button type="button" (click)="openPicker()" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600" aria-label="Choisir des fournisseurs">@</button>
<button *ngIf="(query() || '').length > 0" type="button" (click)="queryUpdate('')" class="text-xs px-2 py-1 rounded hover:bg-slate-700/70" aria-label="Effacer la recherche">×</button>
<button type="submit" class="ml-1 px-3 py-1 rounded bg-red-600 hover:bg-red-500 text-white"
(click)="onSubmit($event)"
[attr.title]="(query() || '').trim().length < 2 ? 'Tapez au moins 2 caractères' : null">
+114 -63
View File
@@ -182,79 +182,21 @@ export class SearchComponent implements AfterViewInit {
// Helper computed: filtered list based on active tag
filteredResults = computed(() => {
const tag = this.filterTag();
const list = this.allVideos();
const provider = this.selectedProviderForView();
if (!Array.isArray(list) || list.length === 0) return [];
if (provider === 'twitch') return list; // Not used for Twitch (separate sections)
if (tag === 'all') return list;
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
const oneYear = 365 * oneDay;
// Duration filters
if (tag === 'short') return list.filter(v => {
const d = Number(v.durationSec || 0);
return d > 0 && d < 4 * 60;
});
if (tag === 'medium') return list.filter(v => {
const d = Number(v.durationSec || 0);
return d >= 4 * 60 && d < 20 * 60;
});
if (tag === 'long') return list.filter(v => {
const d = Number(v.durationSec || 0);
return d >= 20 * 60;
});
// Date filters
if (tag === 'today') return list.filter(v => {
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= oneDay;
});
if (tag === 'this_week') return list.filter(v => {
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= sevenDays;
});
if (tag === 'this_month') return list.filter(v => {
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= thirtyDays;
});
if (tag === 'this_year') return list.filter(v => {
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= oneYear;
});
return list;
const filtered = this.applyTagFilter(list);
return this.sortVideos(filtered);
});
// Filter each provider group separately for sectioned rendering
filteredGroups = computed(() => {
const tag = this.filterTag();
const predicate = this.buildTagPredicate();
const g = this.groups();
const result: Record<string, any[]> = {};
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
const oneYear = 365 * oneDay;
const applyFilter = (list: any[]): any[] => {
if (tag === 'all' || this.selectedProviderForView() === 'twitch') return list;
if (tag === 'short') return list.filter(v => { const d = Number(v.durationSec || 0); return d > 0 && d < 4 * 60; });
if (tag === 'medium') return list.filter(v => { const d = Number(v.durationSec || 0); return d >= 4 * 60 && d < 20 * 60; });
if (tag === 'long') return list.filter(v => { const d = Number(v.durationSec || 0); return d >= 20 * 60; });
if (tag === 'today') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= oneDay; });
if (tag === 'this_week') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= sevenDays; });
if (tag === 'this_month') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= thirtyDays; });
if (tag === 'this_year') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= oneYear; });
return list;
};
Object.entries(g || {}).forEach(([pid, list]) => {
result[pid] = Array.isArray(list) ? applyFilter(list) : [];
result[pid] = Array.isArray(list) ? list.filter(predicate) : [];
});
return result;
});
@@ -431,6 +373,115 @@ export class SearchComponent implements AfterViewInit {
}
private durationSeconds(item: any): number {
const raw = item?.durationSec ?? item?.duration ?? item?.length ?? 0;
if (typeof raw === 'number') {
return Number.isFinite(raw) ? raw : 0;
}
const parsed = Number(raw);
return Number.isFinite(parsed) ? parsed : 0;
}
private publishedTimestamp(item: any): number {
const raw = item?.publishedAt ?? item?.uploadedDate ?? item?.uploadDate ?? item?.published_at ?? item?.uploaded ?? null;
if (typeof raw === 'number') {
return Number.isFinite(raw) ? raw : 0;
}
if (typeof raw === 'string' && raw.trim()) {
const parsed = Date.parse(raw);
if (!Number.isNaN(parsed)) {
return parsed;
}
}
return 0;
}
private viewCount(item: any): number {
const raw = item?.viewCount ?? item?.views ?? item?.stats?.views ?? item?.metrics?.views ?? 0;
if (typeof raw === 'number') {
return Number.isFinite(raw) ? raw : 0;
}
const parsed = Number(raw);
return Number.isFinite(parsed) ? parsed : 0;
}
private buildTagPredicate(): (item: any) => boolean {
const tag = this.filterTag();
const provider = this.selectedProviderForView();
if (provider === 'twitch' || tag === 'all' || tag?.startsWith('twitch')) {
return () => true;
}
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
const oneYear = 365 * oneDay;
switch (tag) {
case 'short':
return (item) => {
const d = this.durationSeconds(item);
return d > 0 && d < 4 * 60;
};
case 'medium':
return (item) => {
const d = this.durationSeconds(item);
return d >= 4 * 60 && d < 20 * 60;
};
case 'long':
return (item) => {
const d = this.durationSeconds(item);
return d >= 20 * 60;
};
case 'today':
return (item) => {
const ts = this.publishedTimestamp(item);
return ts > 0 && (now - ts) <= oneDay;
};
case 'this_week':
return (item) => {
const ts = this.publishedTimestamp(item);
return ts > 0 && (now - ts) <= sevenDays;
};
case 'this_month':
return (item) => {
const ts = this.publishedTimestamp(item);
return ts > 0 && (now - ts) <= thirtyDays;
};
case 'this_year':
return (item) => {
const ts = this.publishedTimestamp(item);
return ts > 0 && (now - ts) <= oneYear;
};
default:
return () => true;
}
}
private applyTagFilter(list: any[]): any[] {
const predicate = this.buildTagPredicate();
return Array.isArray(list) ? list.filter(predicate) : [];
}
private sortVideos(list: any[]): any[] {
if (!Array.isArray(list)) {
return [];
}
const mode = this.sortParam();
if (mode === 'relevance') {
return [...list];
}
const arr = [...list];
if (mode === 'date') {
arr.sort((a, b) => this.publishedTimestamp(b) - this.publishedTimestamp(a));
} else if (mode === 'views') {
arr.sort((a, b) => this.viewCount(b) - this.viewCount(a));
}
return arr;
}
formatViews(views: number): string {
if (views >= 1_000_000_000) return (views / 1_000_000_000).toFixed(1) + 'B';
@@ -104,6 +104,9 @@
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="provider()"></app-provider-badge>
</div>
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">
{{ v.duration | duration }}
</div>
@@ -9,6 +9,7 @@ import { Video } from '../../models/video.model';
import { TranslatePipe } from '../../pipes/translate.pipe';
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
import { map, of, switchMap } from 'rxjs';
const TWITCH_THEME_KEYWORDS: Record<string, string[]> = {
@@ -43,7 +44,7 @@ const TWITCH_THEME_KEYWORDS: Record<string, string[]> = {
standalone: true,
templateUrl: './provider-theme-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, RouterLink, TranslatePipe, LikeButtonComponent, DurationPipe]
imports: [CommonModule, FormsModule, RouterLink, TranslatePipe, LikeButtonComponent, DurationPipe, ProviderBadgeComponent]
})
export class ProviderThemePageComponent implements OnDestroy {
private route = inject(ActivatedRoute);
@@ -1,6 +1,5 @@
<div class="container mx-auto p-4 sm:p-6">
<h2 class="text-3xl font-bold mb-6 text-slate-100 border-l-4 border-red-500 pl-4 flex items-center gap-2">
<span>{{ themes.bySlug(themeSlug())?.emoji }}</span>
<span>{{ themeLabel() }}</span>
</h2>
@@ -29,8 +28,11 @@
class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300">
<div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-40 object-cover">
<div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded" *ngIf="v.duration">
{{ v.duration / 60 | number:'1.0-0' }}:{{ (v.duration % 60) | number:'2.0-0' }}
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="b.provider"></app-provider-badge>
</div>
<div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded" *ngIf="v.duration && v.duration > 0">
{{ v.duration | duration }}
</div>
<div class="absolute bottom-2 left-2">
<app-like-button [videoId]="v.videoId" [provider]="b.provider" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
@@ -7,6 +7,8 @@ import { Provider, InstanceService } from '../../services/instance.service';
import { Video } from '../../models/video.model';
import { TranslatePipe } from '../../pipes/translate.pipe';
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
interface ProviderBlock {
provider: Provider;
@@ -21,7 +23,7 @@ interface ProviderBlock {
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './theme-page.component.html',
imports: [CommonModule, RouterLink, TranslatePipe, LikeButtonComponent]
imports: [CommonModule, RouterLink, TranslatePipe, LikeButtonComponent, ProviderBadgeComponent, DurationPipe]
})
export class ThemePageComponent {
private route = inject(ActivatedRoute);
+7 -1
View File
@@ -287,8 +287,14 @@
<div class="space-y-4">
@for (related of v.relatedStreams; track related.videoId) {
<a [routerLink]="['/watch', related.videoId]" [queryParams]="watchQueryParams(related)" [state]="{ video: related }" class="flex items-start space-x-3 group hover:bg-slate-800/50 p-2 rounded-lg transition">
<div class="w-40 flex-shrink-0">
<div class="relative w-40 flex-shrink-0">
<img [src]="related.thumbnail" [alt]="related.title" class="w-full h-24 object-cover rounded">
<div class="absolute top-1.5 right-1.5">
<app-provider-badge [provider]="related.provider"></app-provider-badge>
</div>
<div *ngIf="related.duration && related.duration > 0" class="absolute bottom-1.5 right-1.5 bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded">
{{ related.duration | duration }}
</div>
</div>
<div class="flex-1">
<h3 class="font-semibold text-sm line-clamp-2 group-hover:text-red-400">{{ related.title }}</h3>
+3 -1
View File
@@ -16,13 +16,15 @@ import { AuthService } from '../../services/auth.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { IframeProgressService } from '../../services/iframe-progress.service';
import { formatAbsoluteFr, formatNumberFr } from '../../utils/date.util';
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
@Component({
selector: 'app-watch',
templateUrl: './watch.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, VideoPlayerComponent, RouterLink, AddToPlaylistComponent]
imports: [CommonModule, VideoPlayerComponent, RouterLink, AddToPlaylistComponent, ProviderBadgeComponent, DurationPipe]
})
export class WatchComponent implements OnDestroy, AfterViewInit {
private route = inject(ActivatedRoute);