feat(watch,themes): transcript en sidebar, seek fiable, player instantane, bande themes repliable
CI / build-and-test (push) Failing after 8m19s

- 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
This commit is contained in:
2026-09-26 09:56:04 -04:00
parent d5ceab7115
commit a70d7b9797
5 changed files with 694 additions and 508 deletions
@@ -1,4 +1,19 @@
<nav class="sticky top-16 left-0 right-0 z-40 w-full relative"
@if (collapsed()) {
<!-- Barre repliée : fine, rouvrable à tout moment -->
<div class="sticky top-16 left-0 right-0 z-40 flex w-full items-center gap-2 border-b border-slate-700/60 bg-slate-800/60 px-3 py-1 backdrop-blur-sm">
<span class="text-xs font-medium text-slate-400">{{ 'themes.title' | t : 'Thèmes' }}</span>
@if (activeThemeLabel(); as label) {
<span class="rounded-full border border-red-500/60 bg-slate-100/10 px-2 py-0.5 text-xs text-white">{{ label }}</span>
}
<button type="button" (click)="toggleCollapsed()" [attr.aria-expanded]="false" aria-controls="themes-bar"
[title]="'themes.show' | t : 'Afficher la bande des thèmes'"
class="ml-auto inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-semibold text-slate-300 transition hover:bg-slate-700 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500">
<span>{{ 'themes.show' | t : 'Afficher' }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
</button>
</div>
} @else {
<nav id="themes-bar" class="sticky top-16 left-0 right-0 z-40 w-full relative"
[ngClass]="{
'bg-slate-800 border-b border-slate-700': isDarkTheme(),
'bg-slate-900 border-b border-slate-800': isBlackTheme(),
@@ -24,6 +39,18 @@
»
</button>
<!-- Replier la bande -->
<button type="button" (click)="toggleCollapsed()" [attr.aria-expanded]="true" aria-controls="themes-bar"
[title]="'themes.hide' | t : 'Masquer la bande des thèmes'"
class="absolute right-12 top-1/2 z-10 hidden h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full shadow transition focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 md:flex"
[ngClass]="{
'bg-white/90 border border-slate-300 text-slate-700 hover:bg-slate-100': isLightTheme(),
'bg-slate-800/90 border border-slate-700 text-slate-200 hover:bg-slate-700': isDarkTheme() || isBlackTheme() || isBlueTheme()
}"
aria-label="Masquer la bande des thèmes">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd" /></svg>
</button>
<div class="w-full overflow-x-auto no-scrollbar" #scroll (wheel)="onWheel($event)">
<ul role="tablist" aria-label="Themes" class="flex gap-2 px-12 py-2 min-w-max items-center">
<li *ngFor="let t of displayedThemes(); let i = index" class="shrink-0">
@@ -45,6 +72,13 @@
<span>{{ themesSvc.i18nLabel(t) }}</span>
</button>
</li>
<li class="shrink-0 md:hidden">
<button type="button" (click)="toggleCollapsed()"
class="px-3 py-1.5 rounded-full text-xs font-semibold text-slate-400 border border-slate-700 whitespace-nowrap">
{{ 'themes.hide' | t : 'Masquer' }}
</button>
</li>
</ul>
</div>
</nav>
}
+24 -1
View File
@@ -2,12 +2,15 @@ import { ChangeDetectionStrategy, Component, ElementRef, QueryList, ViewChild, V
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],
imports: [CommonModule, TranslatePipe],
templateUrl: './themes-nav.component.html'
})
export class ThemesNavComponent implements OnDestroy {
@@ -19,6 +22,26 @@ export class ThemesNavComponent implements OnDestroy {
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>;