mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Implement different DTOs for users and admins (#644)
* Implement different DTOs for users and admins * Fix performance by not creating unnecessary websocket connections
This commit is contained in:
@@ -40,13 +40,17 @@ export default function App() {
|
||||
function ViewWithAuth() {
|
||||
const auth = useAuth();
|
||||
|
||||
initializeLibraryState();
|
||||
initializeGameState();
|
||||
useEffect(() => {
|
||||
if (auth.state.initializing || auth.state.loading) return;
|
||||
|
||||
if (isAdmin(auth)) {
|
||||
initializeScanState();
|
||||
initializePluginState();
|
||||
}
|
||||
initializeLibraryState();
|
||||
initializeGameState();
|
||||
|
||||
if (isAdmin(auth)) {
|
||||
initializeScanState();
|
||||
initializePluginState();
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
return <>
|
||||
<IconContext.Provider value={{size: 20}}>
|
||||
|
||||
@@ -32,8 +32,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
}
|
||||
|
||||
function getConfig(key: string): ConfigEntryDto | undefined {
|
||||
// @ts-ignore
|
||||
return state.state[key];
|
||||
return state.state[key] as ConfigEntryDto | undefined;
|
||||
}
|
||||
|
||||
function getChangedValues(initial: NestedConfig, current: NestedConfig): Record<string, any> {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function SearchBar() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const state = useSnapshot(gameState);
|
||||
const games = state.recentlyUpdated as GameDto[];
|
||||
const games = state.games as GameDto[];
|
||||
|
||||
return <Autocomplete
|
||||
aria-label="Search for games"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {Button, Card, Chip, Tooltip} from "@heroui/react";
|
||||
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
|
||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||
import React from "react";
|
||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
@@ -10,9 +9,10 @@ import {useNavigate} from "react-router";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {gameState} from "Frontend/state/GameState";
|
||||
import IconBackgroundPattern from "Frontend/components/general/IconBackgroundPattern";
|
||||
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||
|
||||
interface LibraryOverviewCardProps {
|
||||
library: LibraryDto;
|
||||
library: LibraryAdminDto;
|
||||
}
|
||||
|
||||
export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
||||
@@ -28,7 +28,7 @@ export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
||||
}
|
||||
|
||||
async function triggerScan(scanType: ScanType) {
|
||||
await LibraryEndpoint.triggerScan(scanType, [library]);
|
||||
await LibraryEndpoint.triggerScan(scanType, [library.id]);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,6 +25,7 @@ import GameUpdateDto from "Frontend/generated/org/gameyfin/app/games/dto/GameUpd
|
||||
import {useMemo, useState} from "react";
|
||||
import EditGameMetadataModal from "Frontend/components/general/modals/EditGameMetadataModal";
|
||||
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
||||
import {GameAdminDto} from "Frontend/dtos/GameDtos";
|
||||
|
||||
interface LibraryManagementGamesProps {
|
||||
library: LibraryDto;
|
||||
@@ -34,12 +35,12 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
||||
const rowsPerPage = 25;
|
||||
|
||||
const state = useSnapshot(gameState);
|
||||
const games = state.gamesByLibraryId[library.id] ? state.gamesByLibraryId[library.id] as GameDto[] : [];
|
||||
const games = state.gamesByLibraryId[library.id] ? state.gamesByLibraryId[library.id] as GameAdminDto[] : [];
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filter, setFilter] = useState<"all" | "confirmed" | "nonConfirmed">("all");
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({column: "title", direction: "ascending"});
|
||||
|
||||
const [selectedGame, setSelectedGame] = useState<GameDto>(games[0]);
|
||||
const [selectedGame, setSelectedGame] = useState<GameAdminDto>(games[0]);
|
||||
const editGameModal = useDisclosure();
|
||||
const matchGameModal = useDisclosure();
|
||||
|
||||
@@ -53,7 +54,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
||||
}, [games, filter, searchTerm]);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return filteredItems.slice().sort((a, b) => {
|
||||
return (filteredItems as GameAdminDto[]).slice().sort((a, b) => {
|
||||
let cmp: number;
|
||||
|
||||
switch (sortDescriptor.column) {
|
||||
@@ -86,7 +87,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
||||
|
||||
|
||||
function getFilteredGames() {
|
||||
let filteredGames = games.filter((game) =>
|
||||
let filteredGames = (games as GameAdminDto[]).filter((game) =>
|
||||
game.metadata.path!!.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
game.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
game.publishers?.some(publisher => publisher.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
@@ -102,7 +103,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
||||
return filteredGames;
|
||||
}
|
||||
|
||||
async function toggleMatchConfirmed(game: GameDto) {
|
||||
async function toggleMatchConfirmed(game: GameAdminDto) {
|
||||
await GameEndpoint.updateGame(
|
||||
{
|
||||
id: game.id,
|
||||
@@ -163,13 +164,13 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
||||
<TableColumn width={1}>Actions</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody emptyContent="Your filter did not match any games." items={pagedItems}>
|
||||
{(item) => (
|
||||
{(item: GameAdminDto) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<Link href={`/game/${item.id}`}
|
||||
color="foreground"
|
||||
className="text-sm"
|
||||
underline="hover">{item.title} ({item.release !== undefined ? new Date(item.release).getFullYear() : "unknown"})
|
||||
underline="hover">{item.title} ({item.release ? new Date(item.release).getFullYear() : "unknown"})
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
+2
-2
@@ -1,4 +1,3 @@
|
||||
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -19,9 +18,10 @@ import {useMemo, useState} from "react";
|
||||
import LibraryUpdateDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryUpdateDto";
|
||||
import {fileNameFromPath, hashCode} from "Frontend/util/utils";
|
||||
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
||||
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||
|
||||
interface LibraryManagementUnmatchedPathsProps {
|
||||
library: LibraryDto;
|
||||
library: LibraryAdminDto;
|
||||
}
|
||||
|
||||
export default function LibraryManagementUnmatchedPaths({library}: LibraryManagementUnmatchedPathsProps) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import Input from "Frontend/components/general/input/Input";
|
||||
import * as Yup from "yup";
|
||||
import DirectoryMappingInput from "Frontend/components/general/input/DirectoryMappingInput";
|
||||
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||
|
||||
interface LibraryCreationModalProps {
|
||||
libraries: LibraryDto[];
|
||||
@@ -23,7 +24,7 @@ export default function LibraryCreationModal({
|
||||
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
|
||||
|
||||
async function createLibrary(library: LibraryDto) {
|
||||
await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation);
|
||||
await LibraryEndpoint.createLibrary(library as LibraryAdminDto, scanAfterCreation);
|
||||
|
||||
addToast({
|
||||
title: "New library created",
|
||||
|
||||
@@ -15,7 +15,6 @@ export default function PluginIcon({
|
||||
blurred = false,
|
||||
showTooltip = true
|
||||
}: PluginIconProps) {
|
||||
|
||||
const icon = plugin.hasLogo
|
||||
?
|
||||
<Image isBlurred={blurred} src={`/images/plugins/${plugin.id}/logo`} width={size} height={size} radius="none"/>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||
|
||||
export interface GameAdminDto extends GameDto {
|
||||
metadata: GameMetadataAdminDto;
|
||||
}
|
||||
|
||||
export interface GameMetadataAdminDto {
|
||||
path?: string | null;
|
||||
fileSize: number;
|
||||
fields?: { [key: string]: GameFieldMetadataDto } | null;
|
||||
originalIds?: { [key: string]: string } | null;
|
||||
downloadCount: number;
|
||||
matchConfirmed: boolean;
|
||||
}
|
||||
|
||||
export interface GameFieldMetadataDto {
|
||||
type: GameFieldMetadataType;
|
||||
source: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export enum GameFieldMetadataType {
|
||||
PLUGIN = 'PLUGIN',
|
||||
USER = 'USER',
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Subscription} from "@vaadin/hilla-frontend";
|
||||
import {proxy} from "valtio/index";
|
||||
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||
import GameEvent from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryEvent";
|
||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||
import Rand from "rand-seed";
|
||||
import GameEvent from "Frontend/generated/org/gameyfin/app/games/dto/GameEvent";
|
||||
|
||||
type GameState = {
|
||||
subscription?: Subscription<GameEvent[]>;
|
||||
@@ -116,7 +116,7 @@ export const gameState = proxy<GameState>({
|
||||
|
||||
/** Subscribe to and process state updates from backend **/
|
||||
export async function initializeGameState() {
|
||||
if (gameState.isLoaded) return gameState;
|
||||
if (gameState.isLoaded) return;
|
||||
|
||||
// Fetch initial library list
|
||||
const initialEntries = await GameEndpoint.getAll();
|
||||
@@ -140,6 +140,4 @@ export async function initializeGameState() {
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return gameState;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export const libraryState = proxy<LibraryState>({
|
||||
|
||||
/** Subscribe to and process state updates from backend **/
|
||||
export async function initializeLibraryState() {
|
||||
if (libraryState.isLoaded) return libraryState;
|
||||
if (libraryState.isLoaded) return;
|
||||
|
||||
// Fetch initial library list
|
||||
const initialEntries = await LibraryEndpoint.getAll();
|
||||
@@ -57,6 +57,4 @@ export async function initializeLibraryState() {
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return libraryState;
|
||||
}
|
||||
@@ -7,9 +7,8 @@ import ImageCarousel from "Frontend/components/general/covers/ImageCarousel";
|
||||
import {Accordion, AccordionItem, addToast, Button, Chip, Link, Tooltip, useDisclosure} from "@heroui/react";
|
||||
import {humanFileSize, isAdmin, toTitleCase} from "Frontend/util/utils";
|
||||
import {DownloadEndpoint} from "Frontend/endpoints/endpoints";
|
||||
import {gameState, initializeGameState} from "Frontend/state/GameState";
|
||||
import {gameState} from "Frontend/state/GameState";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||
import {CheckCircle, Info, MagnifyingGlass, Pencil, Trash, TriangleDashed} from "@phosphor-icons/react";
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
||||
@@ -17,6 +16,7 @@ import EditGameMetadataModal from "Frontend/components/general/modals/EditGameMe
|
||||
import GameUpdateDto from "Frontend/generated/org/gameyfin/app/games/dto/GameUpdateDto";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
import {GameAdminDto} from "Frontend/dtos/GameDtos";
|
||||
|
||||
export default function GameView() {
|
||||
const {gameId} = useParams();
|
||||
@@ -28,7 +28,7 @@ export default function GameView() {
|
||||
const matchGameModal = useDisclosure();
|
||||
|
||||
const state = useSnapshot(gameState);
|
||||
const game = gameId ? state.state[parseInt(gameId)] as GameDto : undefined;
|
||||
const game = gameId ? state.state[parseInt(gameId)] as GameAdminDto : undefined;
|
||||
|
||||
const [downloadOptions, setDownloadOptions] = useState<Record<string, ComboButtonOption>>();
|
||||
|
||||
@@ -49,13 +49,11 @@ export default function GameView() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
initializeGameState().then((state) => {
|
||||
if (!gameId || !state.state[parseInt(gameId)]) {
|
||||
navigate("/", {replace: true});
|
||||
}
|
||||
document.title = game ? game.title : "Gameyfin";
|
||||
});
|
||||
}, [gameId]);
|
||||
if (state.isLoaded && (!gameId || !state.state[parseInt(gameId)])) {
|
||||
navigate("/", {replace: true});
|
||||
}
|
||||
document.title = game ? game.title : "Gameyfin";
|
||||
}, [gameId, state]);
|
||||
|
||||
async function toggleMatchConfirmed() {
|
||||
if (!game) return;
|
||||
|
||||
@@ -6,8 +6,9 @@ import {ArrowLeft} from "@phosphor-icons/react";
|
||||
import LibraryManagementDetails from "Frontend/components/general/library/LibraryManagementDetails";
|
||||
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||
import {libraryState} from "Frontend/state/LibraryState";
|
||||
import LibraryManagementUnmatchedPaths from "Frontend/components/general/library/LibraryManagementUnmatchedPaths";
|
||||
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||
|
||||
|
||||
export default function LibraryManagementView() {
|
||||
@@ -17,12 +18,10 @@ export default function LibraryManagementView() {
|
||||
const state = useSnapshot(libraryState);
|
||||
|
||||
useEffect(() => {
|
||||
initializeLibraryState().then((state) => {
|
||||
if (!libraryId || !state.state[parseInt(libraryId)]) {
|
||||
navigate("/administration/libraries");
|
||||
}
|
||||
});
|
||||
}, [libraryId]);
|
||||
if (state.isLoaded && (!libraryId || !state.state[parseInt(libraryId)])) {
|
||||
navigate("/administration/libraries");
|
||||
}
|
||||
}, [state, libraryId]);
|
||||
|
||||
return libraryId && state.state[parseInt(libraryId)] && <div className="flex flex-col gap-4">
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
@@ -31,23 +30,18 @@ export default function LibraryManagementView() {
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">Manage library</h1>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<LibraryHeader library={state.state[libraryId]} className="h-32"/>
|
||||
{/* @ts-ignore */}
|
||||
<LibraryHeader library={state.state[parseInt(libraryId)] as LibraryAdminDto} className="h-32"/>
|
||||
<Tabs color="primary" fullWidth
|
||||
selectedKey={hash.length > 0 ? hash : "#details"}
|
||||
onSelectionChange={(newKey) => navigate(newKey.toString(), {replace: true})}>
|
||||
<Tab key="#details" title="Details">
|
||||
{/* @ts-ignore */}
|
||||
<LibraryManagementDetails library={state.state[libraryId]}/>
|
||||
<LibraryManagementDetails library={state.state[parseInt(libraryId)] as LibraryAdminDto}/>
|
||||
</Tab>
|
||||
<Tab key="#games" title="Games">
|
||||
{/* @ts-ignore */}
|
||||
<LibraryManagementGames library={state.state[libraryId]}/>
|
||||
<LibraryManagementGames library={state.state[parseInt(libraryId)] as LibraryAdminDto}/>
|
||||
</Tab>
|
||||
<Tab key="#unmatched-paths" title="Unmatched paths">
|
||||
{/* @ts-ignore */}
|
||||
<LibraryManagementUnmatchedPaths library={state.state[libraryId]}/>
|
||||
<LibraryManagementUnmatchedPaths library={state.state[parseInt(libraryId)] as LibraryAdminDto}/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||
import {libraryState} from "Frontend/state/LibraryState";
|
||||
import {gameState} from "Frontend/state/GameState";
|
||||
import React, {useEffect} from "react";
|
||||
import {useNavigate, useParams} from "react-router";
|
||||
@@ -13,13 +13,11 @@ export default function LibraryView() {
|
||||
const games = (libraryId ? useSnapshot(gameState).gamesByLibraryId[parseInt(libraryId!!)] || [] : []) as GameDto[];
|
||||
|
||||
useEffect(() => {
|
||||
initializeLibraryState().then((state) => {
|
||||
if (!libraryId || !state.state[parseInt(libraryId)]) {
|
||||
navigate("/", {replace: true});
|
||||
}
|
||||
document.title = state.state[parseInt(libraryId!!)]?.name || "Gameyfin";
|
||||
});
|
||||
}, [libraryId]);
|
||||
if (libraries.isLoaded && (!libraryId || !libraries.state[parseInt(libraryId)])) {
|
||||
navigate("/", {replace: true});
|
||||
}
|
||||
document.title = libraries.state[parseInt(libraryId!!)]?.name || "Gameyfin";
|
||||
}, [libraryId, libraries]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
Reference in New Issue
Block a user