mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
The squash-merge of PR #419 was performed against an outdated PR base that predated several main-side refactors and features. The resulting squash inadvertently re-introduced files that had already been removed (`src-tauri/src/audio.rs` monolith, `app-icon.png`) and reverted main's content for ~20 files (`src-tauri/src/lib.rs` decompose, `src/App.tsx` animation-pause, `src/components/AlbumRow.tsx` headerExtra, etc). This commit: * Restores all collateral-damage files to their pre-#419 main state (5662dafe), including the audio module split, lib decompose, Cargo.toml `windows` dep, animation-pause-on-blur logic, and `reducedAnimations` toggle plumbing in WaveformSeek/Settings. * Keeps the genuine #419 work intact: tri-state duration toggle, position counter, persistent Now Playing collapse, animated EQ indicator (in QueuePanel.tsx, 8 locales, authStore, components.css). * Merges authStore.ts so both `reducedAnimations` (from #426) and `queueNowPlayingCollapsed` (from #419) coexist. * Adds the `.eq-bars.paused` CSS rule manually since components.css needed a5662dafebase + the single #419 addition. * Fixes a latent type error in OverlayScrollArea.tsx (`useRef<T>(null)` is `RefObject<T>` / read-only under current `@types/react`; widened to `useRef<T | null>(null)` so the existing `wrapRef.current = el` assignment compiles). Verified locally with `cargo check` and `npm run build` — both green.
This commit is contained in:
committed by
GitHub
parent
18b4a982ef
commit
e44e6dcdf4
@@ -13,9 +13,11 @@ interface Props {
|
||||
moreText?: string;
|
||||
onLoadMore?: () => Promise<void>;
|
||||
showRating?: boolean;
|
||||
/** Optional content rendered in the row header, left of the scroll-nav. */
|
||||
headerExtra?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AlbumRow({ title, titleLink, albums, moreLink, moreText, onLoadMore, showRating }: Props) {
|
||||
export default function AlbumRow({ title, titleLink, albums, moreLink, moreText, onLoadMore, showRating, headerExtra }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const navigate = useNavigate();
|
||||
@@ -71,15 +73,16 @@ export default function AlbumRow({ title, titleLink, albums, moreLink, moreText,
|
||||
<h2 className="section-title" style={{ marginBottom: 0 }}>{title}</h2>
|
||||
)}
|
||||
<div className="album-row-nav">
|
||||
<button
|
||||
className={`nav-btn ${!showLeft ? 'disabled' : ''}`}
|
||||
{headerExtra}
|
||||
<button
|
||||
className={`nav-btn ${!showLeft ? 'disabled' : ''}`}
|
||||
onClick={() => scroll('left')}
|
||||
disabled={!showLeft}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
<button
|
||||
className={`nav-btn ${!showRight ? 'disabled' : ''}`}
|
||||
<button
|
||||
className={`nav-btn ${!showRight ? 'disabled' : ''}`}
|
||||
onClick={() => scroll('right')}
|
||||
disabled={!showRight}
|
||||
>
|
||||
|
||||
@@ -119,6 +119,7 @@ const TrackRow = React.memo(function TrackRow({
|
||||
const isActive = currentTrackId === song.id;
|
||||
// Primitive selector: row only re-renders when *this song's* preview state flips.
|
||||
const isPreviewing = usePreviewStore(s => s.previewingId === song.id);
|
||||
const isPreviewAudioStarted = usePreviewStore(s => s.previewingId === song.id && s.audioStarted);
|
||||
|
||||
const renderCell = (colDef: ColDef) => {
|
||||
const key = colDef.key as ColKey;
|
||||
@@ -156,7 +157,7 @@ const TrackRow = React.memo(function TrackRow({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`playlist-suggestion-preview-btn${isPreviewing ? ' is-previewing' : ''}`}
|
||||
className={`playlist-suggestion-preview-btn${isPreviewing ? ' is-previewing' : ''}${isPreviewAudioStarted ? ' audio-started' : ''}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
usePreviewStore.getState().startPreview({ id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt, duration: song.duration }, 'albums');
|
||||
|
||||
@@ -9,7 +9,7 @@ import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useKeybindingsStore, matchInAppBinding } from '../store/keybindingsStore';
|
||||
import { useDragDrop } from '../contexts/DragDropContext';
|
||||
import { useDragDrop, registerQueueDragHitTest } from '../contexts/DragDropContext';
|
||||
import { useWindowVisibility } from '../hooks/useWindowVisibility';
|
||||
import { IS_LINUX } from '../utils/platform';
|
||||
import MiniContextMenu from './MiniContextMenu';
|
||||
@@ -115,6 +115,18 @@ export default function MiniPlayer() {
|
||||
const [volumeOpen, setVolumeOpen] = useState(false);
|
||||
const ticker = useRef<number | null>(null);
|
||||
const queueScrollRef = useRef<HTMLDivElement>(null);
|
||||
const miniQueueWrapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queueOpen) return;
|
||||
const hitTest = (cx: number, cy: number) => {
|
||||
const el = miniQueueWrapRef.current;
|
||||
if (!el) return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
return cx >= r.left && cx <= r.right && cy >= r.top && cy <= r.bottom;
|
||||
};
|
||||
return registerQueueDragHitTest(hitTest);
|
||||
}, [queueOpen]);
|
||||
const volumeBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const volumePopRef = useRef<HTMLDivElement>(null);
|
||||
const [volumePopStyle, setVolumePopStyle] = useState<React.CSSProperties>({});
|
||||
@@ -415,6 +427,37 @@ export default function MiniPlayer() {
|
||||
return () => el.removeEventListener('psy-drop', onPsyDrop);
|
||||
}, [queueOpen, state.queue.length]);
|
||||
|
||||
// Drop outside the mini queue strip → remove (same UX as main QueuePanel).
|
||||
useEffect(() => {
|
||||
if (!queueOpen) return;
|
||||
const onDocPsyDrop = (e: Event) => {
|
||||
const d = (e as CustomEvent<{ data?: string; clientX?: number; clientY?: number }>).detail;
|
||||
if (!d?.data) return;
|
||||
const cx = d.clientX;
|
||||
const cy = d.clientY;
|
||||
if (typeof cx !== 'number' || typeof cy !== 'number') return;
|
||||
let parsed: { type?: string; index?: number } | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(d.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (parsed?.type !== 'queue_reorder' || typeof parsed.index !== 'number') return;
|
||||
const wrap = miniQueueWrapRef.current;
|
||||
if (!wrap) return;
|
||||
const r = wrap.getBoundingClientRect();
|
||||
const inside =
|
||||
cx >= r.left && cx <= r.right && cy >= r.top && cy <= r.bottom;
|
||||
if (inside) return;
|
||||
psyDragFromIdxRef.current = null;
|
||||
dropTargetRef.current = null;
|
||||
setDropTarget(null);
|
||||
emit('mini:remove', { index: parsed.index }).catch(() => {});
|
||||
};
|
||||
document.addEventListener('psy-drop', onDocPsyDrop);
|
||||
return () => document.removeEventListener('psy-drop', onDocPsyDrop);
|
||||
}, [queueOpen]);
|
||||
|
||||
// Auto-scroll the current track into view when the queue expands.
|
||||
useEffect(() => {
|
||||
if (!queueOpen) return;
|
||||
@@ -633,6 +676,7 @@ export default function MiniPlayer() {
|
||||
|
||||
{queueOpen && (
|
||||
<OverlayScrollArea
|
||||
wrapRef={miniQueueWrapRef}
|
||||
viewportRef={queueScrollRef}
|
||||
className="mini-queue-wrap"
|
||||
viewportClassName="mini-queue"
|
||||
|
||||
@@ -20,6 +20,8 @@ export type OverlayScrollAreaProps = {
|
||||
viewportScrollBehaviorAuto?: boolean;
|
||||
/** Ref to the scrollable element (querySelector, scrollIntoView, etc.). */
|
||||
viewportRef?: React.Ref<HTMLDivElement>;
|
||||
/** Ref to the outer wrapper (incl. overlay scrollbar rail). */
|
||||
wrapRef?: React.Ref<HTMLDivElement>;
|
||||
/** Optional id on the viewport (e.g. main app scroll for route pages). */
|
||||
viewportId?: string;
|
||||
/** Optional wheel handler on the scrollable viewport. */
|
||||
@@ -49,11 +51,12 @@ export default function OverlayScrollArea({
|
||||
railInset = 'none',
|
||||
viewportScrollBehaviorAuto = false,
|
||||
viewportRef: viewportRefProp,
|
||||
wrapRef: wrapRefProp,
|
||||
viewportId,
|
||||
viewportOnWheel,
|
||||
viewportOnTouchMove,
|
||||
}: OverlayScrollAreaProps) {
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const [meta, setMeta] = useState({ thumbH: 0, thumbT: 0, visible: false });
|
||||
|
||||
@@ -109,6 +112,11 @@ export default function OverlayScrollArea({
|
||||
assignRef(viewportRefProp, el);
|
||||
};
|
||||
|
||||
const setWrapNode = (el: HTMLDivElement | null) => {
|
||||
wrapRef.current = el;
|
||||
assignRef(wrapRefProp, el);
|
||||
};
|
||||
|
||||
const rootClass = [
|
||||
'overlay-scroll',
|
||||
RAIL_INSET_CLASS[railInset],
|
||||
@@ -121,7 +129,7 @@ export default function OverlayScrollArea({
|
||||
const viewportClass = ['overlay-scroll__viewport', viewportClassName].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} className={rootClass} onMouseMove={onMouseMove}>
|
||||
<div ref={setWrapNode} className={rootClass} onMouseMove={onMouseMove}>
|
||||
<div
|
||||
id={viewportId}
|
||||
ref={setViewportNode}
|
||||
|
||||
@@ -237,6 +237,7 @@ export default function PlayerBar() {
|
||||
const playSlotRef = useRef<HTMLSpanElement>(null);
|
||||
const scheduleRemaining = usePlaybackScheduleRemaining();
|
||||
const isPreviewing = usePreviewStore(s => s.previewingId !== null);
|
||||
const previewAudioStarted = usePreviewStore(s => s.audioStarted);
|
||||
const previewingTrack = usePreviewStore(s => s.previewingTrack);
|
||||
|
||||
const isRadio = !!currentRadio;
|
||||
@@ -312,7 +313,7 @@ export default function PlayerBar() {
|
||||
<>
|
||||
<footer
|
||||
ref={playerBarRef}
|
||||
className={`player-bar ${floatingPlayerBar ? 'floating' : ''}${showPreviewMeta ? ' is-previewing' : ''}`}
|
||||
className={`player-bar ${floatingPlayerBar ? 'floating' : ''}${showPreviewMeta ? ' is-previewing' : ''}${showPreviewMeta && previewAudioStarted ? ' audio-started' : ''}`}
|
||||
style={floatingPlayerBar ? floatingStyle : undefined}
|
||||
role="region"
|
||||
aria-label={t('player.regionLabel')}
|
||||
|
||||
@@ -825,7 +825,7 @@ export function SeekbarPreview({
|
||||
}
|
||||
};
|
||||
const tick = () => {
|
||||
if (document.hidden || window.__psyHidden) {
|
||||
if (document.hidden || window.__psyHidden || window.__psyBlurred) {
|
||||
pollId = window.setTimeout(() => {
|
||||
pollId = null;
|
||||
tick();
|
||||
@@ -912,11 +912,14 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
const waveformBins = usePlayerStore(s => s.waveformBins);
|
||||
const duration = usePlayerStore(s => s.currentTrack?.duration ?? 0);
|
||||
const seekbarStyle = useAuthStore(s => s.seekbarStyle);
|
||||
const reducedAnimations = useAuthStore(s => s.reducedAnimations);
|
||||
|
||||
// Ref so the subscription callback (closed over at mount) can read the
|
||||
// current style without stale-closure issues.
|
||||
const styleRef = useRef(seekbarStyle);
|
||||
styleRef.current = seekbarStyle;
|
||||
const reducedRef = useRef(reducedAnimations);
|
||||
reducedRef.current = reducedAnimations;
|
||||
|
||||
useEffect(() => {
|
||||
if (!trackId) {
|
||||
@@ -1051,6 +1054,7 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
animStateRef.current = makeAnimState();
|
||||
let rafId: number | null = null;
|
||||
let pollId: number | null = null;
|
||||
let skip = false;
|
||||
const stop = () => {
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId);
|
||||
@@ -1062,14 +1066,22 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
}
|
||||
};
|
||||
const tick = () => {
|
||||
if (document.hidden || window.__psyHidden) {
|
||||
if (document.hidden || window.__psyHidden || window.__psyBlurred) {
|
||||
pollId = window.setTimeout(() => {
|
||||
pollId = null;
|
||||
tick();
|
||||
}, 400);
|
||||
return;
|
||||
}
|
||||
animStateRef.current.time += 0.016;
|
||||
// 30 fps cap when reducedAnimations is on: skip every other rAF, advance
|
||||
// animation time by a doubled delta so wave speed stays the same.
|
||||
if (reducedRef.current && skip) {
|
||||
skip = false;
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
skip = reducedRef.current;
|
||||
animStateRef.current.time += reducedRef.current ? 0.032 : 0.016;
|
||||
drawSeekbar(canvas, seekbarStyle, heightsRef.current, progressRef.current, bufferedRef.current, animStateRef.current);
|
||||
rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user