mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
198 lines
5.3 KiB
TypeScript
198 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
useState,
|
|
useLayoutEffect,
|
|
useEffect,
|
|
useRef,
|
|
useCallback,
|
|
memo,
|
|
} from "react";
|
|
import { FixedSizeGrid as Grid } from "react-window";
|
|
import { useRouter } from "next/navigation";
|
|
import { ClientSkin } from "./SkinScroller";
|
|
import {
|
|
SCREENSHOT_WIDTH,
|
|
SKIN_RATIO,
|
|
} from "../../../legacy-client/src/constants";
|
|
|
|
type Props = {
|
|
initialSkins: ClientSkin[];
|
|
getSkins: (sessionId: string, offset: number) => Promise<ClientSkin[]>;
|
|
sessionId: string;
|
|
};
|
|
|
|
type CellProps = {
|
|
columnIndex: number;
|
|
rowIndex: number;
|
|
style: React.CSSProperties;
|
|
data: {
|
|
skins: ClientSkin[];
|
|
columnCount: number;
|
|
requestSkinsIfNeeded: (index: number) => void;
|
|
};
|
|
};
|
|
|
|
// Extract Cell as a separate component so we can use hooks
|
|
const GridCell = memo(({ columnIndex, rowIndex, style, data }: CellProps) => {
|
|
const { skins, columnCount, requestSkinsIfNeeded } = data;
|
|
const router = useRouter();
|
|
const index = rowIndex * columnCount + columnIndex;
|
|
const skin = skins[index];
|
|
|
|
// Request more skins if this cell needs data
|
|
useEffect(() => {
|
|
if (!skin) {
|
|
requestSkinsIfNeeded(index);
|
|
}
|
|
}, [skin, index, requestSkinsIfNeeded]);
|
|
|
|
if (!skin) {
|
|
return <div style={style} />;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
...style,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
backgroundColor: "#1a1a1a",
|
|
padding: "2px",
|
|
boxSizing: "border-box",
|
|
cursor: "pointer",
|
|
}}
|
|
onClick={() => {
|
|
router.push(`/scroll/skin/${skin.md5}`);
|
|
}}
|
|
>
|
|
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
|
<img
|
|
src={skin.screenshotUrl}
|
|
alt={skin.fileName}
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "block",
|
|
imageRendering: "pixelated",
|
|
objectFit: "cover",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
GridCell.displayName = "GridCell";
|
|
|
|
// Calculate grid dimensions based on window width
|
|
// Skins will be scaled to fill horizontally across multiple columns
|
|
function getGridDimensions(windowWidth: number) {
|
|
const scale = 1.0; // Can be adjusted for different sizes
|
|
const columnCount = Math.max(
|
|
1,
|
|
Math.floor(windowWidth / (SCREENSHOT_WIDTH * scale))
|
|
);
|
|
const columnWidth = windowWidth / columnCount;
|
|
const rowHeight = columnWidth * SKIN_RATIO;
|
|
return { columnWidth, rowHeight, columnCount };
|
|
}
|
|
|
|
export default function InfiniteScrollGrid({
|
|
initialSkins,
|
|
getSkins,
|
|
sessionId,
|
|
}: Props) {
|
|
const [skins, setSkins] = useState<ClientSkin[]>(initialSkins);
|
|
const [fetching, setFetching] = useState(false);
|
|
const [windowWidth, setWindowWidth] = useState(0);
|
|
const [windowHeight, setWindowHeight] = useState(0);
|
|
const gridRef = useRef<Grid>(null);
|
|
const requestedIndicesRef = useRef<Set<number>>(new Set());
|
|
|
|
// Track window size
|
|
useLayoutEffect(() => {
|
|
function updateSize() {
|
|
setWindowWidth(window.innerWidth);
|
|
setWindowHeight(window.innerHeight);
|
|
}
|
|
updateSize();
|
|
window.addEventListener("resize", updateSize);
|
|
return () => window.removeEventListener("resize", updateSize);
|
|
}, []);
|
|
|
|
// Scroll to top when window width changes (column count changes)
|
|
useEffect(() => {
|
|
if (gridRef.current && windowWidth > 0) {
|
|
gridRef.current.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
|
}
|
|
}, [windowWidth]);
|
|
|
|
// Function to request more skins when a cell needs data
|
|
const requestSkinsIfNeeded = useCallback(
|
|
(index: number) => {
|
|
// Only fetch if this index is beyond our current data
|
|
if (index >= skins.length) {
|
|
// Calculate which batch this index belongs to
|
|
const batchSize = 50; // Fetch in batches
|
|
const batchStart = Math.floor(skins.length / batchSize) * batchSize;
|
|
|
|
// Only fetch if we haven't already requested this batch
|
|
if (!requestedIndicesRef.current.has(batchStart) && !fetching) {
|
|
requestedIndicesRef.current.add(batchStart);
|
|
setFetching(true);
|
|
getSkins(sessionId, batchStart)
|
|
.then((newSkins) => {
|
|
setSkins((prevSkins) => [...prevSkins, ...newSkins]);
|
|
setFetching(false);
|
|
})
|
|
.catch(() => {
|
|
requestedIndicesRef.current.delete(batchStart);
|
|
setFetching(false);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
[skins.length, fetching, sessionId, getSkins]
|
|
);
|
|
|
|
const { columnWidth, rowHeight, columnCount } =
|
|
getGridDimensions(windowWidth);
|
|
|
|
if (windowWidth === 0 || windowHeight === 0) {
|
|
return null; // Don't render until we have window dimensions
|
|
}
|
|
|
|
const rowCount = Math.ceil(skins.length / columnCount);
|
|
|
|
const itemData = {
|
|
skins,
|
|
columnCount,
|
|
requestSkinsIfNeeded,
|
|
};
|
|
|
|
return (
|
|
<div style={{ backgroundColor: "#1a1a1a" }}>
|
|
<Grid
|
|
ref={gridRef}
|
|
columnCount={columnCount}
|
|
columnWidth={columnWidth}
|
|
height={windowHeight}
|
|
rowCount={rowCount}
|
|
rowHeight={rowHeight}
|
|
width={windowWidth}
|
|
itemData={itemData}
|
|
overscanRowCount={2}
|
|
style={{
|
|
scrollbarWidth: "none", // Firefox
|
|
msOverflowStyle: "none", // IE and Edge
|
|
}}
|
|
className="hide-scrollbar"
|
|
>
|
|
{GridCell}
|
|
</Grid>
|
|
</div>
|
|
);
|
|
}
|