first commit

This commit is contained in:
2025-09-14 23:05:30 -04:00
commit 2866d74b32
122 changed files with 95474 additions and 0 deletions
@@ -0,0 +1,104 @@
import { ChangeDetectionStrategy, Component, ElementRef, QueryList, ViewChild, ViewChildren, inject, signal, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { ThemesService } from '../../services/themes.service';
@Component({
selector: 'app-themes-nav',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
templateUrl: './themes-nav.component.html'
})
export class ThemesNavComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
themesSvc = inject(ThemesService);
themes = () => this.themesSvc.themes();
activeSlug = this.themesSvc.activeSlug;
displayedThemes = () => this.themes();
@ViewChildren('pill', { read: ElementRef }) pills!: QueryList<ElementRef<HTMLButtonElement>>;
@ViewChild('scroll', { static: false }) scrollRef?: ElementRef<HTMLDivElement>;
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);
}
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(): string {
try {
const fromAttr = document.documentElement.getAttribute('data-theme');
if (fromAttr) return fromAttr;
const fromStorage = localStorage.getItem('newtube.theme');
const val = fromStorage || 'system';
if (val === 'system') {
// Resolve to light/dark using media query so styles can be deterministic
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light';
}
return val;
} catch {
// Safe fallback: prefer dark for safety in most UIs
const prefersDark = typeof window !== 'undefined' && (window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false);
return prefersDark ? 'dark' : 'light';
}
}
isLightTheme(): boolean {
const t = this.getCurrentTheme();
return t === 'light' || t === 'system';
}
isDarkTheme(): boolean { return this.getCurrentTheme() === 'dark'; }
isBlackTheme(): boolean { return this.getCurrentTheme() === 'black'; }
isBlueTheme(): boolean { return this.getCurrentTheme() === 'blue'; }
}