import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; /** * Bouton-menu flottant (FAB) partagé entre /watch et /shorts. * * Pastille circulaire translucide en haut à gauche du lecteur (façon commandes * overlay des players YouTube/Dailymotion) qui ouvre un panneau déroulant en * « verre dépoli » avec en-tête (titre + fermeture) et contenu projeté via * `ng-content`. Animation d'ouverture/fermeture fluide (fondu + glissement + * échelle), fermeture au clic extérieur et à Échap. * * Usage : ` …items… ` * dans un conteneur `position: relative` (le `:host` se place en absolu). */ @Component({ selector: 'app-player-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule], // Les pages /shorts hijackent `window:wheel` / `window:touchstart|end` pour // naviguer d'un short à l'autre : sans ces stopPropagation, tout geste SUR le // panneau ouvert scrolle la vidéo au lieu du menu (impossible d'atteindre le // bas du menu). Arrêté ici = corrigé une fois pour /watch ET /shorts. host: { '(wheel)': '$event.stopPropagation()', '(touchstart)': '$event.stopPropagation()', '(touchend)': '$event.stopPropagation()', // /shorts suit aussi le doigt en `touchmove` : sans ça, glisser le FAB // déplacerait AUSSI le cadre (deux drags pour un geste). '(touchmove)': '$event.stopPropagation()', }, template: ` `, styles: [ `:host { position: absolute; top: 0.5rem; left: 0.5rem; z-index: 30; display: block; }`, ], }) export class PlayerMenuComponent implements AfterViewInit { @Input() open = false; @Input() title = 'Options'; @Output() openChange = new EventEmitter(); constructor(private el: ElementRef) {} toggle(): void { // Un glissement se termine par un clic : on ne doit pas ouvrir le menu. if (Date.now() < this.suppressClickUntil) return; this.openChange.emit(!this.open); } close(): void { if (this.open) this.openChange.emit(false); } @HostListener('document:click', ['$event']) onDocumentClick(event: Event): void { if (!this.open) return; const target = event.target as Node | null; if (target && this.el.nativeElement.contains(target)) return; this.close(); } @HostListener('document:keydown.escape') onEscape(): void { this.close(); } // ------------------------------------------------------------------------- // Déplacement du FAB (glisser-déposer dans le lecteur) // // Position stockée en % du conteneur : elle suit la taille du cadre (desktop // grand écran comme mobile) et est persistée (localStorage) — c'est un // réglage de confort, pas un état de lecture. // ------------------------------------------------------------------------- private readonly POS_KEY = 'newtube:player-menu.pos'; private dragging = false; private moved = false; private startX = 0; private startY = 0; private baseLeft = 0; private baseTop = 0; private suppressClickUntil = 0; ngAfterViewInit(): void { this.restorePos(); } onPointerDown(ev: PointerEvent): void { if (ev.pointerType === 'mouse' && ev.button !== 0) return; const host = this.el.nativeElement; const parent = host.offsetParent as HTMLElement | null; if (!parent) return; const hr = host.getBoundingClientRect(); const pr = parent.getBoundingClientRect(); // On fige la position courante en px : le CSS peut ancrer à gauche OU à // droite (le cadre /shorts inverse l'ancrage), l'inline prime sur les deux. host.style.left = `${hr.left - pr.left}px`; host.style.top = `${hr.top - pr.top}px`; host.style.right = 'auto'; host.style.bottom = 'auto'; this.dragging = true; this.moved = false; this.startX = ev.clientX; this.startY = ev.clientY; this.baseLeft = hr.left - pr.left; this.baseTop = hr.top - pr.top; try { (ev.currentTarget as HTMLElement | null)?.setPointerCapture(ev.pointerId); } catch {} } onPointerMove(ev: PointerEvent): void { if (!this.dragging) return; const dx = ev.clientX - this.startX; const dy = ev.clientY - this.startY; // Seuil anti-clic : en dessous, c'est un clic, pas un glissement. if (!this.moved && Math.hypot(dx, dy) < 6) return; this.moved = true; const host = this.el.nativeElement; const parent = host.offsetParent as HTMLElement | null; if (!parent) return; const pr = parent.getBoundingClientRect(); const hr = host.getBoundingClientRect(); const left = Math.min(Math.max(0, this.baseLeft + dx), Math.max(0, pr.width - hr.width)); const top = Math.min(Math.max(0, this.baseTop + dy), Math.max(0, pr.height - hr.height)); host.style.left = `${left}px`; host.style.top = `${top}px`; } onPointerUp(): void { if (!this.dragging) return; this.dragging = false; if (!this.moved) return; this.suppressClickUntil = Date.now() + 400; this.persistPos(); } /** Persiste la position (en %) et réoriente le panneau vers l'intérieur. */ private persistPos(): void { const host = this.el.nativeElement; const parent = host.offsetParent as HTMLElement | null; if (!parent) return; const pr = parent.getBoundingClientRect(); const hr = host.getBoundingClientRect(); if (pr.width <= 0 || pr.height <= 0) return; const x = ((hr.left - pr.left) / pr.width) * 100; const y = ((hr.top - pr.top) / pr.height) * 100; try { localStorage.setItem(this.POS_KEY, JSON.stringify({ x, y })); } catch {} host.style.left = `${x}%`; host.style.top = `${y}%`; this.applyAnchor(x, y); } /** Restaure la position enregistrée (aucun stockage = position CSS). */ private restorePos(): void { let pos: { x: number; y: number } | null = null; try { const raw = localStorage.getItem(this.POS_KEY); const parsed = raw ? JSON.parse(raw) : null; if (parsed && Number.isFinite(parsed?.x) && Number.isFinite(parsed?.y)) pos = parsed; } catch {} if (!pos) return; const host = this.el.nativeElement; host.style.left = `${pos.x}%`; host.style.top = `${pos.y}%`; host.style.right = 'auto'; host.style.bottom = 'auto'; this.applyAnchor(pos.x, pos.y); } /** * Le panneau doit toujours s'ouvrir vers l'INTÉRIEUR du conteneur (sinon * `overflow: hidden` du cadre /shorts le rogne) : classe selon le demi-espace. */ private applyAnchor(x: number, y: number): void { const host = this.el.nativeElement; host.classList.toggle('pm-opens-left', x > 50); host.classList.toggle('pm-opens-right', x <= 50); host.classList.toggle('pm-opens-up', y > 45); } }