Files
psysonic/src/hooks/usePlayerBarAnchoredPopover.ts
T
cucadmuh e8e41752a7 feat(playback): global speed with three strategies (#852)
* feat(playback): global speed with three strategies

Add Settings → Audio and player-bar controls for global playback speed
(speed with auto pitch correction as default, varispeed, manual pitch shift).
Time-stretch runs on a background worker; Orbit sessions force 1.0× passthrough.

* fix(playback): align seekbar, seek, and progress on content timeline

Unify UI timebase across varispeed and preserve strategies: full-track
duration, speed-scaled progress for DSP paths, and content-timeline seeks
without varispeed scaling. Reset the sample counter after seek so clicks
land correctly; restart playback on strategy/enable changes instead of
fragile hot-switching.

* fix(ui): anchor playback speed popover like volume controls

Replace the centered EQ-style modal with a player-bar popover (outside
click, Escape, reposition on scroll). Show compact controls in the bar
and overflow menu; keep strategy hints and labels in Settings only.

* docs(release): CHANGELOG and credits for playback speed (PR #852)

* docs(changelog): add playback speed entry for PR #852

* fix(clippy): simplify raw_counter_samples branch for CI

Collapse duplicate if branches flagged by clippy::if-same-then-else.

* fix(ui): wheel on pitch slider adjusts pitch in speed popover

In compact player-bar controls, scroll over the pitch row changes pitch;
elsewhere in the panel changes speed. Stop propagation so overflow menu
wheel does not tweak volume.

* fix(playback): address PR #852 review and drop ineffective dynamic imports

Translate playback-rate strings for de/fr/es/zh/nb/nl/ro; restamp sample
counter on live preserve-path speed changes; use neutral rate atomics for
radio progress; static-import playerStore in playListenSession (move preview
volume sync to previewPlayerVolumeSync side-effect module).

* fix(i18n): translate playback-rate strategy labels in all locales

Replace leftover English Varispeed/Pitch strings in ru and other non-en
settings blocks so popover strategy buttons and hints read natively.

* fix(i18n): refine German varispeed label to "Tonhöhe folgt dem Tempo"

---------

Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
2026-05-22 18:59:28 +02:00

64 lines
1.9 KiB
TypeScript

import { useEffect, useLayoutEffect, useRef, useState } from 'react';
/** Fixed popover anchored above a player-bar trigger (overflow menu / speed btn). */
export function usePlayerBarAnchoredPopover(width: number, zIndex = 10050) {
const [open, setOpen] = useState(false);
const [popStyle, setPopStyle] = useState<React.CSSProperties>({});
const btnRef = useRef<HTMLButtonElement>(null);
const popRef = useRef<HTMLDivElement>(null);
const updatePopStyle = () => {
const btn = btnRef.current;
if (!btn) return;
const MARGIN = 8;
const r = btn.getBoundingClientRect();
const left = Math.min(
Math.max(r.right - width, MARGIN),
window.innerWidth - width - MARGIN,
);
setPopStyle({
position: 'fixed',
left,
width,
bottom: window.innerHeight - r.top + MARGIN,
zIndex,
});
};
useLayoutEffect(() => {
if (!open) return;
updatePopStyle();
}, [open, width]);
useEffect(() => {
if (!open) return;
const onReposition = () => updatePopStyle();
window.addEventListener('resize', onReposition);
window.addEventListener('scroll', onReposition, true);
return () => {
window.removeEventListener('resize', onReposition);
window.removeEventListener('scroll', onReposition, true);
};
}, [open, width]);
useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
const target = e.target as Node;
if (btnRef.current?.contains(target) || popRef.current?.contains(target)) return;
setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', onDown);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDown);
document.removeEventListener('keydown', onKey);
};
}, [open]);
return { open, setOpen, popStyle, btnRef, popRef };
}