Files
gameyfin/app/src/main/frontend/components/general/covers/CoverGrid.tsx
T
Simon 09953a3f78 Release 2.3.0 (#804)
* chore: bump version to v2.3.0-preview

* Customize start page (#803)

* Update ConfigService to support complex Objects
Implemented tests for ConfigService

* Added DB migration for config table

* Fixed version in banner.txt not being displayed

* Implement Library ordering
Implement "Show recently added games on homepage"

* Fix build.gradle.kts

* FIx bug when creating libraries

* Fix TypeScript errors
Fix library sorting

* Bump actions/checkout from 5 to 6 (#811)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Added automatic scanning using file system watchers (#813)

* Implement collections (#814)

* Backend implementation for collections

* Fix database schema and migration script

* Refactor some config values
Fix ArrayInput not being deactivatable

* Remove "AutoRegisterNewUsers" config option

* Fix bug when removing ignored paths

* Add UI for collections (WIP)

* Fix table actions not synced with state
Fix tests

* Finish implementation of collection feature

* Fix tests

* Bump actions/checkout from 5 to 6 (#815)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix "allow guests to create game requests" not being enabled when guest access is activated

* Fix: Disable loading of EditGameMetadataModal and MatchGameModal in GameView when user is not admin

* Bump actions/checkout from 5 to 6 (#819)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Overhaul startpage (#823)

* WIP: Update start page layout

* Performance improvements (lazy loading and virtualized grids/lists)
Fix various smaller issues

* Implement use of blurhash for all images in backend and covers in frontend

* Fix bugs and test

* Fix code analysis issues

* Remove "UI settings" since they have been made obsolete

* Remove length limit from "image.originalUrl" (#824)

* Remove alpine based image (#825)

* Fix bug when games from library are still in a collection, thus prevention deletion of said library

* Delete image files in background

* Fix layout

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 12:58:14 +01:00

110 lines
3.7 KiB
TypeScript

import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
import {GameCover} from "Frontend/components/general/covers/GameCover";
import type {CellComponentProps} from "react-window";
import {Grid} from "react-window";
import {useEffect, useRef, useState} from "react";
interface CoverGridProps {
games: GameDto[];
}
// Constants for grid layout
const MIN_COLUMN_WIDTH = 180; // Minimum width per item (minmax value from original)
const MAX_COLUMN_WIDTH = 212; // Maximum width per item (minmax value from original)
const GAP = 16; // gap-4 = 1rem = 16px
const ASPECT_RATIO = 12 / 17; // Game cover aspect ratio (width/height)
export default function CoverGrid({games}: CoverGridProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(0);
// Update container width on resize
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth);
}
};
const resizeObserver = new ResizeObserver(updateDimensions);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
updateDimensions();
return () => resizeObserver.disconnect();
}, []);
// Calculate how many columns can fit
const columnCount = Math.max(1, Math.floor((containerWidth + GAP) / (MIN_COLUMN_WIDTH + GAP)));
// Calculate actual column width to distribute space evenly (up to MAX_COLUMN_WIDTH)
const actualColumnWidth = Math.min(
MAX_COLUMN_WIDTH,
Math.floor((containerWidth - (columnCount - 1) * GAP) / columnCount)
);
// Calculate cover height based on width and aspect ratio
// GameCover's size prop is the height, so we need to calculate height from width
const coverHeight = Math.floor(actualColumnWidth / ASPECT_RATIO);
// Calculate row count
const rowCount = Math.ceil(games.length / columnCount);
// Cell renderer for react-window Grid
const Cell = ({
columnIndex,
rowIndex,
style
}: CellComponentProps<{}>) => {
const gameIndex = rowIndex * columnCount + columnIndex;
// Return empty cell if we're past the end of the games array
if (gameIndex >= games.length) {
return <div style={style}/>;
}
const game = games[gameIndex];
return (
<div
style={{
...style,
paddingBottom: GAP,
display: 'flex',
justifyContent: 'center',
boxSizing: 'border-box'
}}
>
<GameCover game={game} interactive={true} size={coverHeight} lazy={true}/>
</div>
);
};
// Column width function to handle the last column differently
const getColumnWidth = (index: number) => {
// Last column doesn't need gap after it
if (index === columnCount - 1) {
return actualColumnWidth;
}
return actualColumnWidth + GAP;
};
return (
<div ref={containerRef} className="w-full">
{containerWidth > 0 && (
<Grid<{}>
columnCount={columnCount}
columnWidth={getColumnWidth}
rowCount={rowCount}
rowHeight={coverHeight + GAP}
defaultWidth={containerWidth}
cellComponent={Cell}
cellProps={{}}
style={{overflowX: 'hidden'}}
/>
)}
</div>
);
}