mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(player-bar): H7 — split PlayerBar.tsx 802 → 354 LOC across 10 files (#670)
* refactor(player-bar): H7 — extract PlaybackTime + RemainingTime + formatTime The two memoized clock components (which update the DOM imperatively from the playbackProgress store without re-rendering PlayerBar) move into components/playerBar/PlaybackClock.tsx. formatTime helper → utils/playerBarHelpers.ts. PlayerBar.tsx: 802 → 765 LOC. * refactor(player-bar): H7 — extract PlayerTrackInfo The cover-art wrap + title/artist marquees + star + last.fm love buttons move into their own component. The new file uses PlayerState['openContextMenu'] for prop typing so the union literal type carries through. PlayerBar.tsx: 765 → 674 LOC. * refactor(player-bar): H7 — extract PlayerTransportControls Stop/Prev/Play/Next/Repeat buttons (with the preview-ring + schedule-badge overlays around play/pause) move into PlayerTransportControls.tsx. The component uses ReturnType<...> on the source hooks to derive its playPauseBind + scheduleRemaining prop types so the new file stays in lockstep with usePlaybackDelayPress + usePlaybackScheduleRemaining. PlayerBar.tsx: 674 → 621 LOC. * refactor(player-bar): H7 — extract PlayerSeekbarSection The waveform / radio progress / time-label block moves into its own component. PlayerSeekbarSection branches on isRadio (AzuraCast progress bar with elapsed+duration when available; LIVE badge otherwise) vs. regular track (WaveformSeek or perf-flag fallback + duration ↔ remaining toggle). PlayerBar.tsx: 621 → 582 LOC. * refactor(player-bar): H7 — extract PlayerVolume + PlayerOverflowMenu + 2 hooks PlayerVolume.tsx is the reusable volume button + slider combo, used in three layouts (inline, full menu, volume-only menu) — `inputId` / `sectionModifier` / `wrapModifier` props handle the variants without class duplication. PlayerOverflowMenu.tsx is the portaled Ellipsis-button menu that hosts EQ / mini-player buttons + a PlayerVolume instance. useFloatingPlayerBar owns the docked/floating layout computation (ResizeObserver on sidebar + queue panel). useUtilityOverflowMenu owns the overflow detection, menu open/mode state, close-on-outside-click / Escape, position recompute on resize/scroll, and the wheel-menu timer. PlayerBar.tsx: 582 → 354 LOC.
This commit is contained in:
committed by
GitHub
parent
383bbbd75f
commit
988806e6b1
@@ -0,0 +1,119 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
/** Wires the utility-overflow Ellipsis button + its portaled menu:
|
||||
* - watches the player-bar width via ResizeObserver and flips `utilityOverflow`
|
||||
* when it crosses the threshold (980 px floating / 1140 px docked)
|
||||
* - owns the menu open state and the 'full' vs. 'volume' display mode
|
||||
* - closes the menu on outside click or Escape
|
||||
* - re-positions the menu (fixed, above the trigger) on resize/scroll
|
||||
* - exposes a volumeWheelMenuTimerRef so the volume-wheel auto-hide handler
|
||||
* can reuse the same timer. */
|
||||
export function useUtilityOverflowMenu(
|
||||
playerBarRef: React.RefObject<HTMLElement | null>,
|
||||
floatingPlayerBar: boolean,
|
||||
) {
|
||||
const [utilityOverflow, setUtilityOverflow] = useState(false);
|
||||
const [utilityMenuOpen, setUtilityMenuOpen] = useState(false);
|
||||
const [utilityMenuMode, setUtilityMenuMode] = useState<'full' | 'volume'>('full');
|
||||
const [utilityMenuStyle, setUtilityMenuStyle] = useState<React.CSSProperties>({});
|
||||
const [suppressOverflowTooltip, setSuppressOverflowTooltip] = useState(false);
|
||||
const utilityMenuRef = useRef<HTMLDivElement>(null);
|
||||
const utilityBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const volumeWheelMenuTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const updateOverflow = () => {
|
||||
const width = playerBarRef.current?.clientWidth ?? window.innerWidth;
|
||||
const threshold = floatingPlayerBar ? 980 : 1140;
|
||||
setUtilityOverflow(width < threshold);
|
||||
};
|
||||
|
||||
updateOverflow();
|
||||
const ro = typeof ResizeObserver !== 'undefined'
|
||||
? new ResizeObserver(updateOverflow)
|
||||
: null;
|
||||
const el = playerBarRef.current;
|
||||
if (ro && el) ro.observe(el);
|
||||
window.addEventListener('resize', updateOverflow);
|
||||
return () => {
|
||||
ro?.disconnect();
|
||||
window.removeEventListener('resize', updateOverflow);
|
||||
};
|
||||
}, [floatingPlayerBar, playerBarRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!utilityOverflow) setUtilityMenuOpen(false);
|
||||
if (!utilityOverflow && volumeWheelMenuTimerRef.current != null) {
|
||||
window.clearTimeout(volumeWheelMenuTimerRef.current);
|
||||
volumeWheelMenuTimerRef.current = null;
|
||||
}
|
||||
}, [utilityOverflow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!utilityMenuOpen) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (utilityBtnRef.current?.contains(target)) return;
|
||||
if (utilityMenuRef.current?.contains(target)) return;
|
||||
setUtilityMenuOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setUtilityMenuOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', onDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [utilityMenuOpen]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (volumeWheelMenuTimerRef.current != null) {
|
||||
window.clearTimeout(volumeWheelMenuTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!utilityMenuOpen) return;
|
||||
const MENU_WIDTH = 238;
|
||||
const MARGIN = 8;
|
||||
const updateMenuPos = () => {
|
||||
const btn = utilityBtnRef.current;
|
||||
if (!btn) return;
|
||||
const r = btn.getBoundingClientRect();
|
||||
const left = Math.min(
|
||||
Math.max(r.right - MENU_WIDTH, MARGIN),
|
||||
window.innerWidth - MENU_WIDTH - MARGIN,
|
||||
);
|
||||
setUtilityMenuStyle({
|
||||
position: 'fixed',
|
||||
left,
|
||||
width: MENU_WIDTH,
|
||||
bottom: window.innerHeight - r.top + 8,
|
||||
zIndex: 10050,
|
||||
});
|
||||
};
|
||||
updateMenuPos();
|
||||
window.addEventListener('resize', updateMenuPos);
|
||||
window.addEventListener('scroll', updateMenuPos, true);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateMenuPos);
|
||||
window.removeEventListener('scroll', updateMenuPos, true);
|
||||
};
|
||||
}, [utilityMenuOpen]);
|
||||
|
||||
return {
|
||||
utilityOverflow,
|
||||
utilityMenuOpen,
|
||||
setUtilityMenuOpen,
|
||||
utilityMenuMode,
|
||||
setUtilityMenuMode,
|
||||
utilityMenuStyle,
|
||||
utilityMenuRef,
|
||||
utilityBtnRef,
|
||||
volumeWheelMenuTimerRef,
|
||||
suppressOverflowTooltip,
|
||||
setSuppressOverflowTooltip,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user