mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
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>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useOrbitStore } from '../store/orbitStore';
|
||||
import { usePlaybackRateStore } from '../store/playbackRateStore';
|
||||
|
||||
/** Re-sync playback rate when Orbit enters or leaves shared playback. */
|
||||
export function usePlaybackRateOrbitSync() {
|
||||
const role = useOrbitStore(s => s.role);
|
||||
const phase = useOrbitStore(s => s.phase);
|
||||
|
||||
useEffect(() => {
|
||||
usePlaybackRateStore.getState().syncToRust();
|
||||
}, [role, phase]);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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 };
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export function useUtilityOverflowMenu(
|
||||
|
||||
useEffect(() => {
|
||||
if (!utilityMenuOpen) return;
|
||||
const MENU_WIDTH = 238;
|
||||
const MENU_WIDTH = utilityMenuMode === 'volume' ? 238 : 320;
|
||||
const MARGIN = 8;
|
||||
const updateMenuPos = () => {
|
||||
const btn = utilityBtnRef.current;
|
||||
@@ -101,7 +101,7 @@ export function useUtilityOverflowMenu(
|
||||
window.removeEventListener('resize', updateMenuPos);
|
||||
window.removeEventListener('scroll', updateMenuPos, true);
|
||||
};
|
||||
}, [utilityMenuOpen]);
|
||||
}, [utilityMenuOpen, utilityMenuMode]);
|
||||
|
||||
return {
|
||||
utilityOverflow,
|
||||
|
||||
Reference in New Issue
Block a user