mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
(WIP) Implement game management
This commit is contained in:
Generated
+800
-800
File diff suppressed because it is too large
Load Diff
+53
-53
@@ -9,22 +9,22 @@
|
|||||||
"@polymer/polymer": "3.5.2",
|
"@polymer/polymer": "3.5.2",
|
||||||
"@react-stately/data": "^3.12.2",
|
"@react-stately/data": "^3.12.2",
|
||||||
"@react-types/shared": "^3.28.0",
|
"@react-types/shared": "^3.28.0",
|
||||||
"@vaadin/bundles": "24.8.0-beta1",
|
"@vaadin/bundles": "24.8.0-rc1",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.8.0-beta2",
|
"@vaadin/hilla-file-router": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-frontend": "24.8.0-beta2",
|
"@vaadin/hilla-frontend": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-lit-form": "24.8.0-beta2",
|
"@vaadin/hilla-lit-form": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-auth": "24.8.0-beta2",
|
"@vaadin/hilla-react-auth": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-crud": "24.8.0-beta2",
|
"@vaadin/hilla-react-crud": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-form": "24.8.0-beta2",
|
"@vaadin/hilla-react-form": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-i18n": "24.8.0-beta2",
|
"@vaadin/hilla-react-i18n": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-signals": "24.8.0-beta2",
|
"@vaadin/hilla-react-signals": "24.8.0-rc1",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.8.0-beta1",
|
"@vaadin/polymer-legacy-adapter": "24.8.0-rc1",
|
||||||
"@vaadin/react-components": "24.8.0-beta1",
|
"@vaadin/react-components": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.8.0-beta1",
|
"@vaadin/vaadin-lumo-styles": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-material-styles": "24.8.0-beta1",
|
"@vaadin/vaadin-material-styles": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.0-beta1",
|
"@vaadin/vaadin-themable-mixin": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
@@ -60,19 +60,19 @@
|
|||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@types/node": "^22.4.0",
|
"@types/node": "^22.4.0",
|
||||||
"@types/react": "18.3.22",
|
"@types/react": "18.3.23",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"@vaadin/hilla-generator-cli": "24.8.0-beta2",
|
"@vaadin/hilla-generator-cli": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-core": "24.8.0-beta2",
|
"@vaadin/hilla-generator-core": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-backbone": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-barrel": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-client": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-model": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-push": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-signals": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-signals": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-subtypes": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-utils": "24.8.0-beta2",
|
"@vaadin/hilla-generator-utils": "24.8.0-rc1",
|
||||||
"@vitejs/plugin-react": "4.5.0",
|
"@vitejs/plugin-react": "4.5.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"async": "3.2.6",
|
"async": "3.2.6",
|
||||||
@@ -148,22 +148,22 @@
|
|||||||
"vaadin": {
|
"vaadin": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polymer/polymer": "3.5.2",
|
"@polymer/polymer": "3.5.2",
|
||||||
"@vaadin/bundles": "24.8.0-beta1",
|
"@vaadin/bundles": "24.8.0-rc1",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.8.0-beta2",
|
"@vaadin/hilla-file-router": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-frontend": "24.8.0-beta2",
|
"@vaadin/hilla-frontend": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-lit-form": "24.8.0-beta2",
|
"@vaadin/hilla-lit-form": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-auth": "24.8.0-beta2",
|
"@vaadin/hilla-react-auth": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-crud": "24.8.0-beta2",
|
"@vaadin/hilla-react-crud": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-form": "24.8.0-beta2",
|
"@vaadin/hilla-react-form": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-i18n": "24.8.0-beta2",
|
"@vaadin/hilla-react-i18n": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-react-signals": "24.8.0-beta2",
|
"@vaadin/hilla-react-signals": "24.8.0-rc1",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.8.0-beta1",
|
"@vaadin/polymer-legacy-adapter": "24.8.0-rc1",
|
||||||
"@vaadin/react-components": "24.8.0-beta1",
|
"@vaadin/react-components": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.8.0-beta1",
|
"@vaadin/vaadin-lumo-styles": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-material-styles": "24.8.0-beta1",
|
"@vaadin/vaadin-material-styles": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.0-beta1",
|
"@vaadin/vaadin-themable-mixin": "24.8.0-rc1",
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
@@ -177,19 +177,19 @@
|
|||||||
"@preact/signals-react-transform": "0.5.1",
|
"@preact/signals-react-transform": "0.5.1",
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@types/react": "18.3.22",
|
"@types/react": "18.3.23",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"@vaadin/hilla-generator-cli": "24.8.0-beta2",
|
"@vaadin/hilla-generator-cli": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-core": "24.8.0-beta2",
|
"@vaadin/hilla-generator-core": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-backbone": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-barrel": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-client": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-model": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-push": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-signals": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-signals": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-subtypes": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.0-beta2",
|
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.0-rc1",
|
||||||
"@vaadin/hilla-generator-utils": "24.8.0-beta2",
|
"@vaadin/hilla-generator-utils": "24.8.0-rc1",
|
||||||
"@vitejs/plugin-react": "4.5.0",
|
"@vitejs/plugin-react": "4.5.0",
|
||||||
"async": "3.2.6",
|
"async": "3.2.6",
|
||||||
"glob": "11.0.2",
|
"glob": "11.0.2",
|
||||||
@@ -205,6 +205,6 @@
|
|||||||
"workbox-core": "7.3.0",
|
"workbox-core": "7.3.0",
|
||||||
"workbox-precaching": "7.3.0"
|
"workbox-precaching": "7.3.0"
|
||||||
},
|
},
|
||||||
"hash": "86f3ea527539716295163ae736b2b548fb6d35efd20c87ae1b109c727817cc71"
|
"hash": "b2ffd3ce9b28bc9b88c070ed3a53ff5a6db02bd4f3127932d7c0be123cf7be25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,108 @@
|
|||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||||
import {Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from "@heroui/react";
|
import {
|
||||||
|
Button,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableColumn,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Tooltip
|
||||||
|
} from "@heroui/react";
|
||||||
import {CheckCircle, Pencil, Trash} from "@phosphor-icons/react";
|
import {CheckCircle, Pencil, Trash} from "@phosphor-icons/react";
|
||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {gameState} from "Frontend/state/GameState";
|
import {gameState} from "Frontend/state/GameState";
|
||||||
|
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
import GameUpdateDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameUpdateDto";
|
||||||
|
import {useMemo, useState} from "react";
|
||||||
|
|
||||||
interface LibraryManagementGamesProps {
|
interface LibraryManagementGamesProps {
|
||||||
library: LibraryDto;
|
library: LibraryDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LibraryManagementGames({library}: LibraryManagementGamesProps) {
|
export default function LibraryManagementGames({library}: LibraryManagementGamesProps) {
|
||||||
|
const rowsPerPage = 25;
|
||||||
|
|
||||||
const state = useSnapshot(gameState);
|
const state = useSnapshot(gameState);
|
||||||
const games = state.gamesByLibraryId[library.id] ? state.gamesByLibraryId[library.id] as GameDto[] : undefined;
|
const games = state.gamesByLibraryId[library.id] ? state.gamesByLibraryId[library.id] as GameDto[] : [];
|
||||||
|
const [filter, setFilter] = useState<"all" | "confirmed" | "nonConfirmed">("all");
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pages = useMemo(() => {
|
||||||
|
return Math.ceil(getFilteredGames().length / rowsPerPage);
|
||||||
|
}, [games, filter]);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const start = (page - 1) * rowsPerPage;
|
||||||
|
const end = start + rowsPerPage;
|
||||||
|
|
||||||
|
return getFilteredGames().slice(start, end);
|
||||||
|
}, [page, games, filter]);
|
||||||
|
|
||||||
|
function getFilteredGames() {
|
||||||
|
if (filter === "confirmed") {
|
||||||
|
return games.filter(g => g.metadata.matchConfirmed);
|
||||||
|
}
|
||||||
|
if (filter === "nonConfirmed") {
|
||||||
|
return games.filter(g => !g.metadata.matchConfirmed);
|
||||||
|
}
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleMatchConfirmed(game: GameDto) {
|
||||||
|
await GameEndpoint.updateGame(
|
||||||
|
{
|
||||||
|
id: game.id,
|
||||||
|
metadata: {matchConfirmed: !game.metadata.matchConfirmed}
|
||||||
|
} as GameUpdateDto
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGame(game: GameDto) {
|
||||||
|
await GameEndpoint.deleteGame(game.id);
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="flex flex-col gap-4">
|
return <div className="flex flex-col gap-4">
|
||||||
<h1 className="text-2xl font-bold">Manage games in library</h1>
|
<h1 className="text-2xl font-bold">Manage games in library</h1>
|
||||||
<Table removeWrapper isStriped isHeaderSticky>
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
|
<Select
|
||||||
|
selectedKeys={[filter]}
|
||||||
|
disallowEmptySelection
|
||||||
|
onSelectionChange={keys => setFilter(Array.from(keys)[0] as any)}
|
||||||
|
className="w-64"
|
||||||
|
>
|
||||||
|
<SelectItem key="all">Show all</SelectItem>
|
||||||
|
<SelectItem key="confirmed">Show only confirmed</SelectItem>
|
||||||
|
<SelectItem key="nonConfirmed">Show only non confirmed</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Table removeWrapper isStriped isHeaderSticky
|
||||||
|
bottomContent={
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
{items.length > 0 &&
|
||||||
|
<Pagination
|
||||||
|
isCompact
|
||||||
|
showControls
|
||||||
|
showShadow
|
||||||
|
color="primary"
|
||||||
|
page={page}
|
||||||
|
total={pages}
|
||||||
|
onChange={(page) => setPage(page)}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableColumn allowsSorting>Game</TableColumn>
|
<TableColumn allowsSorting>Game</TableColumn>
|
||||||
<TableColumn allowsSorting>Added to library</TableColumn>
|
<TableColumn allowsSorting>Added to library</TableColumn>
|
||||||
<TableColumn>Path</TableColumn>
|
<TableColumn>Path</TableColumn>
|
||||||
<TableColumn>Actions</TableColumn>
|
{/* width={1} keeps the column as far to the right as possible*/}
|
||||||
|
<TableColumn width={1}>Actions</TableColumn>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody emptyContent="This library is empty." items={games}>
|
<TableBody emptyContent="Your filter did not match any games." items={items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -35,9 +115,18 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
|||||||
{item.metadata.path}
|
{item.metadata.path}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="flex flex-row gap-2">
|
<TableCell className="flex flex-row gap-2">
|
||||||
<Button isIconOnly size="sm" isDisabled={true}><CheckCircle/></Button>
|
<Button isIconOnly size="sm" onPress={() => toggleMatchConfirmed(item)}>
|
||||||
|
{item.metadata.matchConfirmed ?
|
||||||
|
<Tooltip content="Unconfirm match">
|
||||||
|
<CheckCircle weight="fill" className="fill-success"/>
|
||||||
|
</Tooltip> :
|
||||||
|
<Tooltip content="Confirm match">
|
||||||
|
<CheckCircle/>
|
||||||
|
</Tooltip>}
|
||||||
|
</Button>
|
||||||
<Button isIconOnly size="sm" isDisabled={true}><Pencil/></Button>
|
<Button isIconOnly size="sm" isDisabled={true}><Pencil/></Button>
|
||||||
<Button isIconOnly size="sm" isDisabled={true} color="danger"><Trash/></Button>
|
<Button isIconOnly size="sm" color="danger"
|
||||||
|
onPress={() => deleteGame(item)}><Trash/></Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
|
import {Button, Pagination, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from "@heroui/react";
|
||||||
|
import {Trash} from "@phosphor-icons/react";
|
||||||
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
import {useMemo, useState} from "react";
|
||||||
|
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
|
import {hashCode} from "Frontend/util/utils";
|
||||||
|
|
||||||
|
interface LibraryManagementUnmatchedPathsProps {
|
||||||
|
library: LibraryDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LibraryManagementUnmatchedPaths({library}: LibraryManagementUnmatchedPathsProps) {
|
||||||
|
const rowsPerPage = 25;
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pages = useMemo(() => {
|
||||||
|
return Math.ceil(library.unmatchedPaths!.length / rowsPerPage);
|
||||||
|
}, [library]);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const start = (page - 1) * rowsPerPage;
|
||||||
|
const end = start + rowsPerPage;
|
||||||
|
|
||||||
|
return unmatchedPathItems().slice(start, end);
|
||||||
|
}, [page, library]);
|
||||||
|
|
||||||
|
async function deleteUnmatchedPath(unmatchedPath: string) {
|
||||||
|
const libraryUpdateDto: LibraryUpdateDto = {
|
||||||
|
id: library.id,
|
||||||
|
unmatchedPaths: library.unmatchedPaths!.filter((path) => path !== unmatchedPath)
|
||||||
|
}
|
||||||
|
await LibraryEndpoint.updateLibrary(libraryUpdateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmatchedPathItems(): UnmatchedPathItem[] {
|
||||||
|
return library.unmatchedPaths!.map((path) => ({
|
||||||
|
key: hashCode(path),
|
||||||
|
path: path
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="flex flex-col gap-4">
|
||||||
|
<h1 className="text-2xl font-bold">Manage unmatched paths</h1>
|
||||||
|
<Table removeWrapper isStriped isHeaderSticky
|
||||||
|
bottomContent={
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
{items.length > 0 &&
|
||||||
|
<Pagination
|
||||||
|
isCompact
|
||||||
|
showControls
|
||||||
|
showShadow
|
||||||
|
color="primary"
|
||||||
|
page={page}
|
||||||
|
total={pages}
|
||||||
|
onChange={(page) => setPage(page)}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn allowsSorting>Path</TableColumn>
|
||||||
|
<TableColumn width={1}>Actions</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="This library has no unmatched paths." items={items}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.key}>
|
||||||
|
<TableCell>
|
||||||
|
{item.path}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex flex-row gap-2">
|
||||||
|
<Button isIconOnly size="sm" color="danger"
|
||||||
|
onPress={() => deleteUnmatchedPath(item.path)}><Trash/>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
type UnmatchedPathItem = { key: number; path: string };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React, {useState} from "react";
|
||||||
import {addToast, Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
import {addToast, Button, Checkbox, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
||||||
import {Form, Formik} from "formik";
|
import {Form, Formik} from "formik";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
@@ -20,23 +20,25 @@ export default function LibraryCreationModal({
|
|||||||
onOpenChange
|
onOpenChange
|
||||||
}: LibraryCreationModalProps) {
|
}: LibraryCreationModalProps) {
|
||||||
|
|
||||||
|
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
|
||||||
|
|
||||||
async function createLibrary(library: LibraryDto) {
|
async function createLibrary(library: LibraryDto) {
|
||||||
try {
|
try {
|
||||||
await LibraryEndpoint.createLibrary(library as LibraryDto);
|
await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation);
|
||||||
|
|
||||||
|
addToast({
|
||||||
|
title: "New library created",
|
||||||
|
description: `Library ${library.name} created!`,
|
||||||
|
color: "success"
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast({
|
addToast({
|
||||||
title: "Error creating library",
|
title: "Error creating library",
|
||||||
description: `Library ${library.name} could not be created!`,
|
description: `Library ${library.name} could not be created!`,
|
||||||
color: "warning"
|
color: "warning"
|
||||||
});
|
});
|
||||||
return;
|
throw "Error creating library: " + e;
|
||||||
}
|
}
|
||||||
|
|
||||||
addToast({
|
|
||||||
title: "New library created",
|
|
||||||
description: `Library ${library.name} created!`,
|
|
||||||
color: "success"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,17 +76,21 @@ export default function LibraryCreationModal({
|
|||||||
<DirectoryMappingInput name="directories"/>
|
<DirectoryMappingInput name="directories"/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter className="flex flex-row justify-between">
|
||||||
<Button variant="light" onPress={onClose}>
|
<Checkbox isSelected={scanAfterCreation} onValueChange={setScanAfterCreation}>Scan
|
||||||
Cancel
|
after creation?</Checkbox>
|
||||||
</Button>
|
<div className="flex flex-row">
|
||||||
<Button color="primary"
|
<Button variant="light" onPress={onClose}>
|
||||||
isLoading={formik.isSubmitting}
|
Cancel
|
||||||
isDisabled={formik.isSubmitting}
|
</Button>
|
||||||
type="submit"
|
<Button color="primary"
|
||||||
>
|
isLoading={formik.isSubmitting}
|
||||||
{formik.isSubmitting ? "" : "Add"}
|
isDisabled={formik.isSubmitting}
|
||||||
</Button>
|
type="submit"
|
||||||
|
>
|
||||||
|
{formik.isSubmitting ? "" : "Add"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ export function camelCaseToTitle(text: string): string {
|
|||||||
.replace(/^./, str => str.toUpperCase());
|
.replace(/^./, str => str.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hashCode(string: string) {
|
||||||
|
let hash = 0, i, chr;
|
||||||
|
if (string.length === 0) return hash;
|
||||||
|
for (i = 0; i < string.length; i++) {
|
||||||
|
chr = string.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + chr;
|
||||||
|
hash |= 0; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
export function roleToColor(role: string) {
|
export function roleToColor(role: string) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case "ROLE_SUPERADMIN":
|
case "ROLE_SUPERADMIN":
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import LibraryManagementDetails from "Frontend/components/general/library/Librar
|
|||||||
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||||
|
import LibraryManagementUnmatchedPaths from "Frontend/components/general/library/LibraryManagementUnmatchedPaths";
|
||||||
|
|
||||||
|
|
||||||
export default function LibraryManagementView() {
|
export default function LibraryManagementView() {
|
||||||
@@ -41,7 +42,8 @@ export default function LibraryManagementView() {
|
|||||||
<LibraryManagementGames library={state.state[libraryId]}/>
|
<LibraryManagementGames library={state.state[libraryId]}/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Unmatched paths">
|
<Tab title="Unmatched paths">
|
||||||
<p>Unmatched paths</p>
|
{/* @ts-ignore */}
|
||||||
|
<LibraryManagementUnmatchedPaths library={state.state[libraryId]}/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import de.grimsi.gameyfin.core.Role
|
|||||||
import de.grimsi.gameyfin.games.dto.GameDto
|
import de.grimsi.gameyfin.games.dto.GameDto
|
||||||
import de.grimsi.gameyfin.games.dto.GameEvent
|
import de.grimsi.gameyfin.games.dto.GameEvent
|
||||||
import de.grimsi.gameyfin.games.dto.GameUpdateDto
|
import de.grimsi.gameyfin.games.dto.GameUpdateDto
|
||||||
|
import de.grimsi.gameyfin.libraries.LibraryService
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -12,7 +13,8 @@ import reactor.core.publisher.Flux
|
|||||||
@Endpoint
|
@Endpoint
|
||||||
@PermitAll
|
@PermitAll
|
||||||
class GameEndpoint(
|
class GameEndpoint(
|
||||||
private val gameService: GameService
|
private val gameService: GameService,
|
||||||
|
private val libraryService: LibraryService
|
||||||
) {
|
) {
|
||||||
fun subscribe(): Flux<List<GameEvent>> {
|
fun subscribe(): Flux<List<GameEvent>> {
|
||||||
return GameService.subscribe()
|
return GameService.subscribe()
|
||||||
@@ -24,5 +26,8 @@ class GameEndpoint(
|
|||||||
fun updateGame(game: GameUpdateDto) = gameService.update(game)
|
fun updateGame(game: GameUpdateDto) = gameService.update(game)
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun deleteGame(gameId: Long) = gameService.delete(gameId)
|
fun deleteGame(gameId: Long) {
|
||||||
|
libraryService.deleteGameFromLibrary(gameId)
|
||||||
|
gameService.delete(gameId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -90,6 +90,9 @@ class GameService(
|
|||||||
gameUpdateDto.title?.let { existingGame.title = it }
|
gameUpdateDto.title?.let { existingGame.title = it }
|
||||||
gameUpdateDto.comment?.let { existingGame.comment = it }
|
gameUpdateDto.comment?.let { existingGame.comment = it }
|
||||||
gameUpdateDto.summary?.let { existingGame.summary = it }
|
gameUpdateDto.summary?.let { existingGame.summary = it }
|
||||||
|
gameUpdateDto.metadata?.let { metadata ->
|
||||||
|
metadata.matchConfirmed?.let { existingGame.metadata.matchConfirmed = it }
|
||||||
|
}
|
||||||
|
|
||||||
gameRepository.save(existingGame)
|
gameRepository.save(existingGame)
|
||||||
}
|
}
|
||||||
@@ -129,6 +132,12 @@ class GameService(
|
|||||||
return gameRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Game with id $id not found")
|
return gameRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Game with id $id not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMatchConfirmed(gameId: Long, confirmed: Boolean) {
|
||||||
|
val game = getById(gameId)
|
||||||
|
game.metadata.matchConfirmed = confirmed
|
||||||
|
gameRepository.save(game)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries all metadata plugins for metadata on the provided game title
|
* Queries all metadata plugins for metadata on the provided game title
|
||||||
* Runs the queries concurrently and asynchronously
|
* Runs the queries concurrently and asynchronously
|
||||||
@@ -206,81 +215,91 @@ class GameService(
|
|||||||
metadata.title.takeIf { it.isNotBlank() }?.let { title ->
|
metadata.title.takeIf { it.isNotBlank() }?.let { title ->
|
||||||
if (!metadataMap.containsKey("title")) {
|
if (!metadataMap.containsKey("title")) {
|
||||||
mergedGame.title = title
|
mergedGame.title = title
|
||||||
metadataMap["title"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["title"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.description?.takeIf { it.isNotBlank() }?.let { description ->
|
metadata.description?.takeIf { it.isNotBlank() }?.let { description ->
|
||||||
if (!metadataMap.containsKey("summary")) {
|
if (!metadataMap.containsKey("summary")) {
|
||||||
mergedGame.summary = description
|
mergedGame.summary = description
|
||||||
metadataMap["summary"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["summary"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.coverUrl?.let { coverUrl ->
|
metadata.coverUrl?.let { coverUrl ->
|
||||||
if (!metadataMap.containsKey("coverImage")) {
|
if (!metadataMap.containsKey("coverImage")) {
|
||||||
mergedGame.coverImage = Image(originalUrl = coverUrl.toURL(), type = ImageType.COVER)
|
mergedGame.coverImage = Image(originalUrl = coverUrl.toURL(), type = ImageType.COVER)
|
||||||
metadataMap["coverImage"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["coverImage"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.release?.let { release ->
|
metadata.release?.let { release ->
|
||||||
if (!metadataMap.containsKey("release")) {
|
if (!metadataMap.containsKey("release")) {
|
||||||
mergedGame.release = release
|
mergedGame.release = release
|
||||||
metadataMap["release"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["release"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.userRating?.let { userRating ->
|
metadata.userRating?.let { userRating ->
|
||||||
if (!metadataMap.containsKey("userRating")) {
|
if (!metadataMap.containsKey("userRating")) {
|
||||||
mergedGame.userRating = userRating
|
mergedGame.userRating = userRating
|
||||||
metadataMap["userRating"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["userRating"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.criticRating?.let { criticRating ->
|
metadata.criticRating?.let { criticRating ->
|
||||||
if (!metadataMap.containsKey("criticRating")) {
|
if (!metadataMap.containsKey("criticRating")) {
|
||||||
mergedGame.criticRating = criticRating
|
mergedGame.criticRating = criticRating
|
||||||
metadataMap["criticRating"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["criticRating"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy ->
|
metadata.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy ->
|
||||||
if (!metadataMap.containsKey("publishers")) {
|
if (!metadataMap.containsKey("publishers")) {
|
||||||
mergedGame.publishers =
|
mergedGame.publishers =
|
||||||
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }
|
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }
|
||||||
metadataMap["publishers"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["publishers"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy ->
|
metadata.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy ->
|
||||||
if (!metadataMap.containsKey("developers")) {
|
if (!metadataMap.containsKey("developers")) {
|
||||||
mergedGame.developers =
|
mergedGame.developers =
|
||||||
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }
|
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }
|
||||||
metadataMap["developers"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["developers"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.genres?.takeIf { it.isNotEmpty() }?.let { genres ->
|
metadata.genres?.takeIf { it.isNotEmpty() }?.let { genres ->
|
||||||
if (!metadataMap.containsKey("genres")) {
|
if (!metadataMap.containsKey("genres")) {
|
||||||
mergedGame.genres = genres.toList()
|
mergedGame.genres = genres.toList()
|
||||||
metadataMap["genres"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["genres"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.themes?.takeIf { it.isNotEmpty() }?.let { themes ->
|
metadata.themes?.takeIf { it.isNotEmpty() }?.let { themes ->
|
||||||
if (!metadataMap.containsKey("themes")) {
|
if (!metadataMap.containsKey("themes")) {
|
||||||
mergedGame.themes = themes.toList()
|
mergedGame.themes = themes.toList()
|
||||||
metadataMap["themes"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["themes"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.keywords?.takeIf { it.isNotEmpty() }?.let { keywords ->
|
metadata.keywords?.takeIf { it.isNotEmpty() }?.let { keywords ->
|
||||||
if (!metadataMap.containsKey("keywords")) {
|
if (!metadataMap.containsKey("keywords")) {
|
||||||
mergedGame.keywords = keywords.toList()
|
mergedGame.keywords = keywords.toList()
|
||||||
metadataMap["keywords"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["keywords"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.features?.takeIf { it.isNotEmpty() }?.let { features ->
|
metadata.features?.takeIf { it.isNotEmpty() }?.let { features ->
|
||||||
if (!metadataMap.containsKey("features")) {
|
if (!metadataMap.containsKey("features")) {
|
||||||
mergedGame.features = features.toList()
|
mergedGame.features = features.toList()
|
||||||
metadataMap["features"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["features"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.perspectives?.takeIf { it.isNotEmpty() }?.let { perspectives ->
|
metadata.perspectives?.takeIf { it.isNotEmpty() }?.let { perspectives ->
|
||||||
if (!metadataMap.containsKey("perspectives")) {
|
if (!metadataMap.containsKey("perspectives")) {
|
||||||
mergedGame.perspectives = perspectives.toList()
|
mergedGame.perspectives = perspectives.toList()
|
||||||
metadataMap["perspectives"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["perspectives"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
|
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
|
||||||
@@ -288,13 +307,14 @@ class GameService(
|
|||||||
mergedGame.images = runBlocking {
|
mergedGame.images = runBlocking {
|
||||||
screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) }
|
screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) }
|
||||||
}
|
}
|
||||||
metadataMap["images"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["images"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.videoUrls?.takeIf { it.isNotEmpty() }?.let { videoUrls ->
|
metadata.videoUrls?.takeIf { it.isNotEmpty() }?.let { videoUrls ->
|
||||||
if (!metadataMap.containsKey("videoUrls")) {
|
if (!metadataMap.containsKey("videoUrls")) {
|
||||||
mergedGame.videoUrls = videoUrls.toList()
|
mergedGame.videoUrls = videoUrls.toList()
|
||||||
metadataMap["videoUrls"] = GameFieldMetadata(source = sourcePlugin)
|
metadataMap["videoUrls"] =
|
||||||
|
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,10 +338,33 @@ class GameService(
|
|||||||
fun Game.toDto(): GameDto {
|
fun Game.toDto(): GameDto {
|
||||||
// Helper functions
|
// Helper functions
|
||||||
fun toDto(fieldMetadata: GameFieldMetadata): GameFieldMetadataDto {
|
fun toDto(fieldMetadata: GameFieldMetadata): GameFieldMetadataDto {
|
||||||
return GameFieldMetadataDto(
|
val source = fieldMetadata.source
|
||||||
source = fieldMetadata.source.pluginId,
|
|
||||||
updatedAt = fieldMetadata.updatedAt!!
|
return when (source) {
|
||||||
)
|
is GameFieldPluginSource -> {
|
||||||
|
GameFieldMetadataDto(
|
||||||
|
type = GameFieldMetadataType.PLUGIN,
|
||||||
|
source = source.plugin.pluginId,
|
||||||
|
updatedAt = fieldMetadata.updatedAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameFieldUserSource -> {
|
||||||
|
GameFieldMetadataDto(
|
||||||
|
type = GameFieldMetadataType.USER,
|
||||||
|
source = source.user.id!!,
|
||||||
|
updatedAt = fieldMetadata.updatedAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
GameFieldMetadataDto(
|
||||||
|
type = GameFieldMetadataType.UNKNOWN,
|
||||||
|
source = "unknown source",
|
||||||
|
updatedAt = fieldMetadata.updatedAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toDto(metadata: GameMetadata): GameMetadataDto {
|
fun toDto(metadata: GameMetadata): GameMetadataDto {
|
||||||
@@ -330,7 +373,8 @@ fun Game.toDto(): GameDto {
|
|||||||
downloadCount = metadata.downloadCount,
|
downloadCount = metadata.downloadCount,
|
||||||
path = metadata.path,
|
path = metadata.path,
|
||||||
fields = metadata.fields.mapValues { toDto(it.value) },
|
fields = metadata.fields.mapValues { toDto(it.value) },
|
||||||
originalIds = metadata.originalIds.mapKeys { it.key.pluginId }
|
originalIds = metadata.originalIds.mapKeys { it.key.pluginId },
|
||||||
|
matchConfirmed = metadata.matchConfirmed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
package de.grimsi.gameyfin.games.dto
|
package de.grimsi.gameyfin.games.dto
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class GameFieldMetadataDto(
|
class GameFieldMetadataDto(
|
||||||
val source: String,
|
val type: GameFieldMetadataType,
|
||||||
|
val source: Serializable,
|
||||||
val updatedAt: Instant
|
val updatedAt: Instant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class GameFieldMetadataType {
|
||||||
|
PLUGIN,
|
||||||
|
USER,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
@@ -8,5 +8,6 @@ class GameMetadataDto(
|
|||||||
val fileSize: Long,
|
val fileSize: Long,
|
||||||
val fields: Map<String, GameFieldMetadataDto>?,
|
val fields: Map<String, GameFieldMetadataDto>?,
|
||||||
val originalIds: Map<String, String>?,
|
val originalIds: Map<String, String>?,
|
||||||
val downloadCount: Int
|
val downloadCount: Int,
|
||||||
|
val matchConfirmed: Boolean
|
||||||
)
|
)
|
||||||
@@ -5,4 +5,5 @@ data class GameUpdateDto(
|
|||||||
val title: String?,
|
val title: String?,
|
||||||
val comment: String?,
|
val comment: String?,
|
||||||
val summary: String?,
|
val summary: String?,
|
||||||
|
val metadata: GameUpdateMetadataDto?
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.grimsi.gameyfin.games.dto
|
||||||
|
|
||||||
|
data class GameUpdateMetadataDto(
|
||||||
|
val matchConfirmed: Boolean?
|
||||||
|
)
|
||||||
@@ -78,6 +78,8 @@ class Game(
|
|||||||
|
|
||||||
@Embedded
|
@Embedded
|
||||||
var metadata: GameMetadata
|
var metadata: GameMetadata
|
||||||
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
constructor(path: Path, library: Library) : this(library = library, metadata = GameMetadata(path = path.toString()))
|
constructor(path: Path, library: Library) : this(library = library, metadata = GameMetadata(path = path.toString()))
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.games.entities
|
package de.grimsi.gameyfin.games.entities
|
||||||
|
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
||||||
|
import de.grimsi.gameyfin.users.entities.User
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
import org.hibernate.annotations.UpdateTimestamp
|
import org.hibernate.annotations.UpdateTimestamp
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -11,9 +12,29 @@ class GameFieldMetadata(
|
|||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
@ManyToOne
|
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER)
|
||||||
val source: PluginManagementEntry,
|
val source: GameFieldSource,
|
||||||
|
|
||||||
@UpdateTimestamp
|
@UpdateTimestamp
|
||||||
var updatedAt: Instant? = Instant.now()
|
var updatedAt: Instant? = Instant.now()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Inheritance
|
||||||
|
abstract class GameFieldSource(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
open var id: Long? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class GameFieldPluginSource(
|
||||||
|
@ManyToOne
|
||||||
|
val plugin: PluginManagementEntry
|
||||||
|
) : GameFieldSource()
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class GameFieldUserSource(
|
||||||
|
@ManyToOne
|
||||||
|
val user: User
|
||||||
|
) : GameFieldSource()
|
||||||
@@ -16,5 +16,7 @@ class GameMetadata(
|
|||||||
@ElementCollection
|
@ElementCollection
|
||||||
var originalIds: Map<PluginManagementEntry, String> = emptyMap(),
|
var originalIds: Map<PluginManagementEntry, String> = emptyMap(),
|
||||||
|
|
||||||
var downloadCount: Int = 0
|
var downloadCount: Int = 0,
|
||||||
|
|
||||||
|
var matchConfirmed: Boolean = false
|
||||||
)
|
)
|
||||||
@@ -3,6 +3,9 @@ package de.grimsi.gameyfin.libraries
|
|||||||
import de.grimsi.gameyfin.games.entities.Game
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
import de.grimsi.gameyfin.games.entities.LibraryEntityListener
|
import de.grimsi.gameyfin.games.entities.LibraryEntityListener
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
|
import org.hibernate.annotations.CreationTimestamp
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@EntityListeners(LibraryEntityListener::class)
|
@EntityListeners(LibraryEntityListener::class)
|
||||||
@@ -11,6 +14,13 @@ class Library(
|
|||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(updatable = false)
|
||||||
|
var createdAt: Instant? = null,
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
var updatedAt: Instant? = null,
|
||||||
|
|
||||||
var name: String,
|
var name: String,
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = [CascadeType.ALL])
|
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = [CascadeType.ALL])
|
||||||
@@ -20,5 +30,5 @@ class Library(
|
|||||||
var games: MutableList<Game> = ArrayList(),
|
var games: MutableList<Game> = ArrayList(),
|
||||||
|
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
var unmatchedPaths: MutableList<String> = ArrayList()
|
var unmatchedPaths: MutableList<String> = ArrayList(),
|
||||||
)
|
)
|
||||||
@@ -37,7 +37,8 @@ class LibraryEndpoint(
|
|||||||
libraryService.triggerScan(scanType, libraries)
|
libraryService.triggerScan(scanType, libraries)
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun createLibrary(library: LibraryDto) = libraryService.create(library)
|
fun createLibrary(library: LibraryDto, scanAfterCreation: Boolean = true) =
|
||||||
|
libraryService.create(library, scanAfterCreation)
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun updateLibrary(library: LibraryUpdateDto) = libraryService.update(library)
|
fun updateLibrary(library: LibraryUpdateDto) = libraryService.update(library)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.springframework.data.repository.findByIdOrNull
|
|||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import java.time.Instant
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -85,8 +86,12 @@ class LibraryService(
|
|||||||
* @param library: The library to create or update.
|
* @param library: The library to create or update.
|
||||||
* @return The created or updated LibraryDto object.
|
* @return The created or updated LibraryDto object.
|
||||||
*/
|
*/
|
||||||
fun create(library: LibraryDto) {
|
fun create(library: LibraryDto, scanAfterCreation: Boolean) {
|
||||||
libraryRepository.save(toEntity(library))
|
val newLibrary = libraryRepository.save(toEntity(library))
|
||||||
|
|
||||||
|
if (scanAfterCreation) {
|
||||||
|
triggerScanSingleLibrary(ScanType.QUICK, newLibrary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,19 +102,24 @@ class LibraryService(
|
|||||||
* @throws IllegalArgumentException if the library ID is null or the library is not found.
|
* @throws IllegalArgumentException if the library ID is null or the library is not found.
|
||||||
*/
|
*/
|
||||||
fun update(libraryUpdateDto: LibraryUpdateDto) {
|
fun update(libraryUpdateDto: LibraryUpdateDto) {
|
||||||
val existingLibrary = libraryRepository.findByIdOrNull(libraryUpdateDto.id)
|
var library = libraryRepository.findByIdOrNull(libraryUpdateDto.id)
|
||||||
?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found")
|
?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found")
|
||||||
|
|
||||||
// Update only non-null fields
|
// Update only non-null fields
|
||||||
libraryUpdateDto.name?.let { existingLibrary.name = it }
|
libraryUpdateDto.name?.let { library.name = it }
|
||||||
libraryUpdateDto.directories?.let {
|
libraryUpdateDto.directories?.let {
|
||||||
existingLibrary.directories.clear()
|
library.directories.clear()
|
||||||
existingLibrary.directories.addAll(
|
library.directories.addAll(
|
||||||
it.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
it.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
libraryUpdateDto.unmatchedPaths?.let {
|
||||||
|
library.unmatchedPaths.clear()
|
||||||
|
library.unmatchedPaths.addAll(it)
|
||||||
|
}
|
||||||
|
|
||||||
libraryRepository.save(existingLibrary)
|
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp
|
||||||
|
libraryRepository.save(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,6 +131,17 @@ class LibraryService(
|
|||||||
libraryRepository.deleteById(libraryId)
|
libraryRepository.deleteById(libraryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteGameFromLibrary(gameId: Long) {
|
||||||
|
val game = gameService.getById(gameId)
|
||||||
|
var library = game.library
|
||||||
|
|
||||||
|
library.games.removeIf { it.id == gameId }
|
||||||
|
library.unmatchedPaths.add(game.metadata.path)
|
||||||
|
|
||||||
|
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp
|
||||||
|
libraryRepository.save(library)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper function to trigger a scan for a list of libraries.
|
* Wrapper function to trigger a scan for a list of libraries.
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +154,10 @@ class LibraryService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun triggerScanSingleLibrary(scanType: ScanType, library: Library) {
|
||||||
|
triggerScan(scanType, listOf(library.toDto()))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers a quick scan for a list of libraries.
|
* Triggers a quick scan for a list of libraries.
|
||||||
* A quick scan will only scan for new games and deleted games, but will not touch existing games.
|
* A quick scan will only scan for new games and deleted games, but will not touch existing games.
|
||||||
@@ -287,6 +312,7 @@ class LibraryService(
|
|||||||
addGamesToLibrary(persistedGames, library)
|
addGamesToLibrary(persistedGames, library)
|
||||||
|
|
||||||
// 6. Persist library
|
// 6. Persist library
|
||||||
|
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp
|
||||||
libraryRepository.save(library)
|
libraryRepository.save(library)
|
||||||
|
|
||||||
progress.currentStep = LibraryScanStep(description = "Finished")
|
progress.currentStep = LibraryScanStep(description = "Finished")
|
||||||
@@ -340,6 +366,7 @@ fun Library.toDto(): LibraryDto {
|
|||||||
name = this.name,
|
name = this.name,
|
||||||
directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) },
|
directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) },
|
||||||
games = this.games.mapNotNull { it.id },
|
games = this.games.mapNotNull { it.id },
|
||||||
stats = statsDto
|
stats = statsDto,
|
||||||
|
unmatchedPaths = this.unmatchedPaths
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,6 @@ data class LibraryDto(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val directories: List<DirectoryMappingDto>,
|
val directories: List<DirectoryMappingDto>,
|
||||||
val games: List<Long>?,
|
val games: List<Long>?,
|
||||||
val stats: LibraryStatsDto?
|
val stats: LibraryStatsDto?,
|
||||||
|
val unmatchedPaths: List<String>? = emptyList()
|
||||||
)
|
)
|
||||||
@@ -4,4 +4,5 @@ data class LibraryUpdateDto(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val directories: List<DirectoryMappingDto>? = null,
|
val directories: List<DirectoryMappingDto>? = null,
|
||||||
|
val unmatchedPaths: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user