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((() => { 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>; @ViewChild('scroll', { static: false }) scrollRef?: ElementRef; // Signal to force re-evaluation when data-theme changes private themeChanged = signal(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'; } }