mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
7c724a642f
* chore(eslint): add eslint toolchain and configs * fix(eslint): resolve gradual-config errors (rules-of-hooks, no-empty, prefer-const, …) * chore(eslint): clear unused vars in config, contexts, app and test * chore(eslint): clear unused vars in api and music-network * chore(eslint): clear unused vars in utils * chore(eslint): clear unused vars in store * chore(eslint): clear unused vars in cover and hooks * chore(eslint): clear unused vars in components * chore(eslint): clear unused vars in pages * chore(eslint): remove explicit any in src * chore(eslint): align react-hooks exhaustive-deps * chore(eslint): zero gradual config on src * chore(eslint): strict hook rules in store and utils * chore(eslint): strict hook rules in hooks and cover * chore(eslint): strict hook rules in components * chore(eslint): strict hook rules in pages and contexts * chore(eslint): document scripts ignore in eslint config * chore(eslint): add lint script and zero strict findings * chore(eslint): address review round 1 (gradual 0/0, per-site disable reasons) * chore(eslint): tighten two set-state disable comments (review round 2 LOW) * chore(nix): sync npmDepsHash with package-lock.json * docs(changelog): add Under the Hood entry for ESLint setup (PR #1165) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { useLayoutEffect, useRef, useState } from 'react';
|
|
import { useLocation, useNavigationType, type NavigationType } from 'react-router-dom';
|
|
import {
|
|
peekArtistBrowseScrollRestore,
|
|
useArtistBrowseSessionStore,
|
|
} from '../store/artistBrowseSessionStore';
|
|
import { shouldRestoreArtistBrowseSession } from '../utils/navigation/albumDetailNavigation';
|
|
|
|
type PendingScroll = {
|
|
scrollTop: number;
|
|
visibleCount: number;
|
|
};
|
|
|
|
export type UseArtistsBrowseScrollRestoreArgs = {
|
|
serverId: string;
|
|
scrollBodyEl: HTMLElement | null;
|
|
visibleCount: number;
|
|
loading: boolean;
|
|
loadingMore: boolean;
|
|
hasMore: boolean;
|
|
loadMore: () => void;
|
|
};
|
|
|
|
export type UseArtistsBrowseScrollRestoreResult = {
|
|
isScrollRestorePending: boolean;
|
|
};
|
|
|
|
function readPendingScrollRestore(
|
|
serverId: string,
|
|
navigationType: NavigationType,
|
|
locationState: unknown,
|
|
): PendingScroll | null {
|
|
if (!shouldRestoreArtistBrowseSession(navigationType, locationState) || !serverId) return null;
|
|
return peekArtistBrowseScrollRestore(serverId);
|
|
}
|
|
|
|
/** Restore Artists in-page scroll after returning from artist detail. */
|
|
export function useArtistsBrowseScrollRestore({
|
|
serverId,
|
|
scrollBodyEl,
|
|
visibleCount,
|
|
loading,
|
|
loadingMore,
|
|
hasMore,
|
|
loadMore,
|
|
}: UseArtistsBrowseScrollRestoreArgs): UseArtistsBrowseScrollRestoreResult {
|
|
const navigationType = useNavigationType();
|
|
const location = useLocation();
|
|
const initRef = useRef(false);
|
|
const pendingRef = useRef<PendingScroll | null>(null);
|
|
const doneRef = useRef(false);
|
|
|
|
// React Compiler refs rule: ref used as a once-only init guard (checked before first assignment); not render data.
|
|
// eslint-disable-next-line react-hooks/refs
|
|
if (!initRef.current) {
|
|
initRef.current = true;
|
|
// React Compiler refs rule: ref kept in sync with the latest value for use in effects/handlers/cleanup; not render data.
|
|
// eslint-disable-next-line react-hooks/refs
|
|
pendingRef.current = readPendingScrollRestore(serverId, navigationType, location.state);
|
|
}
|
|
|
|
const [isScrollRestorePending, setIsScrollRestorePending] = useState(
|
|
() => readPendingScrollRestore(serverId, navigationType, location.state) !== null,
|
|
);
|
|
|
|
// React Compiler immutability rule: intentional imperative mutation of an external/DOM target inside an effect.
|
|
// eslint-disable-next-line react-hooks/immutability
|
|
useLayoutEffect(() => {
|
|
const pending = pendingRef.current;
|
|
if (doneRef.current || !pending) return;
|
|
if (!scrollBodyEl || loading) return;
|
|
|
|
const needsMore = visibleCount < pending.visibleCount && hasMore;
|
|
if (needsMore) {
|
|
if (!loadingMore) loadMore();
|
|
return;
|
|
}
|
|
if (loadingMore) return;
|
|
|
|
// React Compiler immutability rule: intentional imperative mutation of an external/DOM target inside an effect.
|
|
// eslint-disable-next-line react-hooks/immutability
|
|
scrollBodyEl.scrollTop = pending.scrollTop;
|
|
scrollBodyEl.dispatchEvent(new Event('scroll', { bubbles: false }));
|
|
pendingRef.current = null;
|
|
doneRef.current = true;
|
|
// React Compiler set-state-in-effect rule: state set from a DOM/layout measurement.
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setIsScrollRestorePending(false);
|
|
useArtistBrowseSessionStore.getState().clearReturnStash(serverId);
|
|
}, [
|
|
scrollBodyEl,
|
|
visibleCount,
|
|
loading,
|
|
loadingMore,
|
|
hasMore,
|
|
loadMore,
|
|
serverId,
|
|
]);
|
|
|
|
return { isScrollRestorePending };
|
|
}
|