Files
NewTube/src/components/themes/themes-nav.component.ts
T
bruno a70d7b9797
CI / build-and-test (push) Failing after 8m19s
feat(watch,themes): transcript en sidebar, seek fiable, player instantane, bande themes repliable
- watch: panneau transcription deplace en tete de sidebar au-dessus de
  A suivre (+ scroll auto a l'ouverture sur mobile)
- seek transcript: seekTo postMessage YouTube (sans rechargement),
  time= Twitch VOD, t= Odysee, start= Rumble (start/dm/pt existants)
- watch: lecteur monte des les parametres de route (embed natif),
  squelettes partiels pendant /details au lieu du plein ecran
- themes-nav: bouton replier/deplier persistant (localStorage),
  barre fine avec theme actif quand repliee, cles i18n FR/EN
2026-09-26 09:56:04 -04:00

152 lines
5.0 KiB
TypeScript

import { ChangeDetectionStrategy, Component, ElementRef, QueryList, ViewChild, ViewChildren, inject, signal, effect, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { ThemesService } from '../../services/themes.service';
import { TranslatePipe } from '../../pipes/translate.pipe';
const THEMES_BAR_COLLAPSED_KEY = 'newtube.themesBarCollapsed';
@Component({
selector: 'app-themes-nav',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TranslatePipe],
templateUrl: './themes-nav.component.html'
})
export class ThemesNavComponent implements OnDestroy {
private router = inject(Router);
private route = inject(ActivatedRoute);
themesSvc = inject(ThemesService);
themes = () => this.themesSvc.themes();
activeSlug = this.themesSvc.activeSlug;
displayedThemes = () => this.themes();
/** Bande repliée/dépliée (persistée) : la barre reste masquable en permanence. */
collapsed = signal<boolean>((() => {
try { return localStorage.getItem(THEMES_BAR_COLLAPSED_KEY) === '1'; } catch { return false; }
})());
toggleCollapsed(): void {
const next = !this.collapsed();
this.collapsed.set(next);
try { localStorage.setItem(THEMES_BAR_COLLAPSED_KEY, next ? '1' : '0'); } catch {}
}
activeThemeLabel(): string {
try {
const slug = this.activeSlug();
const found = this.themes().find((t) => t.slug === slug);
if (found) return this.themesSvc.i18nLabel(found);
} catch {}
return '';
}
@ViewChildren('pill', { read: ElementRef }) pills!: QueryList<ElementRef<HTMLButtonElement>>;
@ViewChild('scroll', { static: false }) scrollRef?: ElementRef<HTMLDivElement>;
// Signal to force re-evaluation when data-theme changes
private themeChanged = signal<number>(0);
constructor() {
// Reflect current URL into active slug
this.router.events.subscribe((e) => {
if (e instanceof NavigationEnd) this.syncFromUrl();
});
// Also on init
setTimeout(() => this.syncFromUrl(), 0);
// Effect to listen for theme changes on document.documentElement
effect(() => {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'system';
// Trigger re-evaluation when theme changes
this.themeChanged.update(n => n + 1);
});
}
ngOnDestroy() {
// Nothing to cleanup
}
private syncFromUrl() {
const url = this.router.url || '';
const m = url.match(/#?\/?(?:p\/[^/]+\/)?t\/([^/?#]+)/i);
if (m && m[1]) this.activeSlug.set(m[1]);
}
goToTheme(slug: string) {
this.activeSlug.set(slug);
this.router.navigate(['/t', slug]);
}
onKeydown(ev: KeyboardEvent, index: number) {
const list = this.pills?.toArray() || [];
if (!list.length) return;
if (ev.key === 'ArrowRight') {
ev.preventDefault();
const next = list[(index + 1) % list.length];
next?.nativeElement?.focus();
next?.nativeElement?.scrollIntoView({ inline: 'center', behavior: 'smooth', block: 'nearest' });
} else if (ev.key === 'ArrowLeft') {
ev.preventDefault();
const prev = list[(index - 1 + list.length) % list.length];
prev?.nativeElement?.focus();
prev?.nativeElement?.scrollIntoView({ inline: 'center', behavior: 'smooth', block: 'nearest' });
}
}
scrollBy(px: number) {
const el = this.scrollRef?.nativeElement;
if (!el) return;
el.scrollBy({ left: px, behavior: 'smooth' });
}
onWheel(ev: WheelEvent) {
const el = this.scrollRef?.nativeElement;
if (!el) return;
// Horizontal scroll with mouse wheel while hovering the bar
if (Math.abs(ev.deltaY) > Math.abs(ev.deltaX)) {
ev.preventDefault();
el.scrollLeft += ev.deltaY;
}
}
// Theme helpers (read from document attribute/localStorage)
private getCurrentTheme(): 'light' | 'dark' | 'black' | 'blue' | 'system' {
try {
const fromAttr = document.documentElement.getAttribute('data-theme');
if (fromAttr) return fromAttr as any;
const fromStorage = localStorage.getItem('newtube.theme');
return (fromStorage as any) || 'system';
} catch {
return 'system';
}
}
private resolveThemeSlug(): 'light' | 'dark' | 'black' | 'blue' {
const cur = this.getCurrentTheme();
if (cur === 'system') {
const prefersDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light';
}
return cur as any;
}
isLightTheme(): boolean {
this.themeChanged(); // Trigger re-evaluation
return this.resolveThemeSlug() === 'light';
}
isDarkTheme(): boolean {
this.themeChanged(); // Trigger re-evaluation
return this.resolveThemeSlug() === 'dark';
}
isBlackTheme(): boolean {
this.themeChanged(); // Trigger re-evaluation
return this.resolveThemeSlug() === 'black';
}
isBlueTheme(): boolean {
this.themeChanged(); // Trigger re-evaluation
return this.resolveThemeSlug() === 'blue';
}
}