From 9794ecc1dd99ab92637b752ceb76437ffde4b7e1 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 18 May 2025 17:38:35 +0200 Subject: [PATCH] Implement realtime UI for plugins Refactor PluginEndpoint Switch from Set to List for a minor performance boost --- gameyfin/package-lock.json | 10 +++ gameyfin/package.json | 6 +- .../administration/PluginManagement.tsx | 17 +++-- .../administration/withConfigPage.tsx | 10 +-- .../components/general/PluginLogo.tsx | 2 +- .../general/PluginManagementSection.tsx | 35 ++------- .../general/cards/PluginManagementCard.tsx | 28 ++----- .../general/modals/PluginDetailsModal.tsx | 36 +++------ .../general/modals/PluginPrioritiesModal.tsx | 8 +- .../src/main/frontend/state/ConfigState.ts | 18 ++--- .../src/main/frontend/state/PluginState.ts | 74 +++++++++++++++++++ .../grimsi/gameyfin/config/ConfigEndpoint.kt | 25 ++----- .../grimsi/gameyfin/config/ConfigService.kt | 26 ++++--- .../grimsi/gameyfin/core/SetupDataLoader.kt | 4 +- .../core/filesystem/FilesystemScanResult.kt | 6 +- .../core/filesystem/FilesystemService.kt | 6 +- .../gameyfin/core/plugins/PluginEndpoint.kt | 42 +++++++++++ .../plugins/config/PluginConfigEndpoint.kt | 28 ------- .../plugins/config/PluginConfigService.kt | 29 ++------ .../plugins/{management => dto}/PluginDto.kt | 7 +- .../core/plugins/dto/PluginUpdateDto.kt | 12 +++ .../management/GameyfinPluginManager.kt | 6 +- .../management/PluginManagementEndpoint.kt | 39 ---------- .../management/PluginManagementService.kt | 61 +++++++-------- .../de/grimsi/gameyfin/games/GameService.kt | 28 +++---- .../de/grimsi/gameyfin/games/entities/Game.kt | 18 ++--- .../games/repositories/GameRepository.kt | 2 +- .../de/grimsi/gameyfin/libraries/Library.kt | 6 +- .../gameyfin/libraries/LibraryScanResult.kt | 8 +- .../gameyfin/libraries/LibraryService.kt | 12 +-- .../libraries/dto/LibraryUpdateDto.kt | 2 +- .../templates/MessageTemplateEndpoint.kt | 4 +- .../de/grimsi/gameyfin/setup/SetupService.kt | 2 +- .../de/grimsi/gameyfin/users/RoleService.kt | 4 +- .../de/grimsi/gameyfin/users/UserService.kt | 7 +- .../grimsi/gameyfin/users/dto/UserInfoDto.kt | 2 +- .../de/grimsi/gameyfin/users/entities/User.kt | 2 +- 37 files changed, 318 insertions(+), 314 deletions(-) create mode 100644 gameyfin/src/main/frontend/state/PluginState.ts create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginEndpoint.kt delete mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/config/PluginConfigEndpoint.kt rename gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/{management => dto}/PluginDto.kt (56%) create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginUpdateDto.kt delete mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEndpoint.kt diff --git a/gameyfin/package-lock.json b/gameyfin/package-lock.json index c35e260..213c0cf 100644 --- a/gameyfin/package-lock.json +++ b/gameyfin/package-lock.json @@ -54,6 +54,7 @@ "remark-breaks": "^4.0.0", "swiper": "^11.2.6", "valtio": "^2.1.5", + "valtio-reactive": "^0.1.2", "yup": "^1.6.1" }, "devDependencies": { @@ -18190,6 +18191,15 @@ } } }, + "node_modules/valtio-reactive": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/valtio-reactive/-/valtio-reactive-0.1.2.tgz", + "integrity": "sha512-9Zv/tFiFWQWEBzfDikJgY9lkQ6CXf4T+Rsk08AKQMMZVmI5YvkAS7qFnRtwd1uVPNT/wsK+QcKiFHBvjCRohYQ==", + "license": "MIT", + "peerDependencies": { + "valtio": ">=2.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/gameyfin/package.json b/gameyfin/package.json index d776053..2f94d1d 100644 --- a/gameyfin/package.json +++ b/gameyfin/package.json @@ -49,6 +49,7 @@ "remark-breaks": "^4.0.0", "swiper": "^11.2.6", "valtio": "^2.1.5", + "valtio-reactive": "^0.1.2", "yup": "^1.6.1" }, "devDependencies": { @@ -138,7 +139,8 @@ "react-player": "$react-player", "react-markdown": "$react-markdown", "remark-breaks": "$remark-breaks", - "valtio": "$valtio" + "valtio": "$valtio", + "valtio-reactive": "$valtio-reactive" }, "vaadin": { "dependencies": { @@ -199,6 +201,6 @@ "workbox-core": "7.3.0", "workbox-precaching": "7.3.0" }, - "hash": "c57af53043f6c6a9f0b03c75c28c3fde0bbfd828f9ce0e179263959c61ec888d" + "hash": "dc682332ca36d64f455f6e13888e1ffcca97e888cbad8d356973e830f7463a10" } } \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx index ce859e3..0dad18f 100644 --- a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx @@ -1,22 +1,29 @@ -import React from "react"; -import {Divider} from "@heroui/react"; +import React, {useEffect} from "react"; import {PluginManagementSection} from "Frontend/components/general/PluginManagementSection"; +import {initializePluginState, pluginState} from "Frontend/state/PluginState"; +import {useSnapshot} from "valtio/react"; export default function PluginManagement() { // Defined manually for now to control the layout (order of categories) const pluginTypes = ["GameMetadataProvider", "DownloadProvider"]; - return ( + const state = useSnapshot(pluginState); + + useEffect(() => { + initializePluginState(); + }, []); + + return state.isLoaded && (

Plugins

-
{pluginTypes.map(type => - + // @ts-ignore + )}
diff --git a/gameyfin/src/main/frontend/components/administration/withConfigPage.tsx b/gameyfin/src/main/frontend/components/administration/withConfigPage.tsx index 5bf9f48..7ee261d 100644 --- a/gameyfin/src/main/frontend/components/administration/withConfigPage.tsx +++ b/gameyfin/src/main/frontend/components/administration/withConfigPage.tsx @@ -5,7 +5,7 @@ import {Form, Formik} from "formik"; import {Button, Skeleton} from "@heroui/react"; import {Check, Info} from "@phosphor-icons/react"; import {SmallInfoField} from "Frontend/components/general/SmallInfoField"; -import {configState, initializeConfig, NestedConfig} from "Frontend/state/ConfigState"; +import {configState, initializeConfigState, NestedConfig} from "Frontend/state/ConfigState"; import {useSnapshot} from "valtio/react"; export default function withConfigPage(WrappedComponent: React.ComponentType, title: String, validationSchema?: any) { @@ -16,7 +16,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType { - initializeConfig(); + initializeConfigState(); }, []); useEffect(() => { @@ -26,14 +26,14 @@ export default function withConfigPage(WrappedComponent: React.ComponentType { - const changed = getChangedValues(state.configNested, values); + const changed = getChangedValues(state.config, values); await ConfigEndpoint.update({updates: changed}); setConfigSaved(true); } function getConfig(key: string): ConfigEntryDto | undefined { // @ts-ignore - return state.configEntries[key]; + return state.state[key]; } function getChangedValues(initial: NestedConfig, current: NestedConfig): Record { @@ -86,7 +86,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType {state.isLoaded ? ([]); +export function PluginManagementSection({type, plugins}: PluginManagementSectionProps) { const pluginPrioritiesModal = useDisclosure(); - useEffect(() => { - PluginManagementEndpoint.getPlugins(pluginType).then((response) => { - let sortedPlugins: PluginDto[] = response - .filter(p => !!p) - .sort((a: PluginDto, b: PluginDto) => { - if (a.name === undefined || b.name === undefined) return 0; - return a.name.localeCompare(b.name); - }); - - setPlugins(sortedPlugins); - }); - }, []); - - function updatePlugin(plugin: PluginDto) { - setPlugins(plugins.map(p => p.id === plugin.id ? plugin : p)); - } - return (
-

{camelCaseToTitle(pluginType)}

+

{camelCaseToTitle(type)}

- {plugins.map((plugin) => + {plugins.map((plugin) => + )}
diff --git a/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx b/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx index 67883c4..3dcbd93 100644 --- a/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx @@ -15,8 +15,6 @@ import { WarningCircle, XCircle } from "@phosphor-icons/react"; -import {PluginManagementEndpoint} from "Frontend/generated/endpoints"; -import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto"; import PluginState from "Frontend/generated/org/pf4j/PluginState"; import React, {ReactNode, useEffect, useState} from "react"; import PluginDetailsModal from "Frontend/components/general/modals/PluginDetailsModal"; @@ -24,16 +22,15 @@ import PluginLogo from "Frontend/components/general/PluginLogo"; import PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel"; import PluginConfigValidationResult from "Frontend/generated/de/grimsi/gameyfin/core/plugins/config/PluginConfigValidationResult"; +import {PluginEndpoint} from "Frontend/generated/endpoints"; +import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto"; -export function PluginManagementCard({plugin, updatePlugin}: { - plugin: PluginDto, - updatePlugin: (plugin: PluginDto) => void -}) { +export function PluginManagementCard({plugin}: { plugin: PluginDto }) { const pluginDetailsModal = useDisclosure(); const [configValidationResult, setConfigValidationResult] = useState(undefined); useEffect(() => { - PluginManagementEndpoint.validatePluginConfig(plugin.id).then((response: PluginConfigValidationResult | undefined) => { + PluginEndpoint.validatePluginConfig(plugin.id).then((response: PluginConfigValidationResult | undefined) => { if (response === undefined) return; setConfigValidationResult(response); }); @@ -55,6 +52,7 @@ export function PluginManagementCard({plugin, updatePlugin}: { case PluginState.DISABLED: return "warning"; case PluginState.FAILED: + case PluginState.STOPPED: return "danger"; default: return "default"; @@ -67,6 +65,7 @@ export function PluginManagementCard({plugin, updatePlugin}: { return ; case PluginState.DISABLED: return ; + case PluginState.STOPPED: case PluginState.FAILED: return ; case PluginState.UNLOADED: @@ -131,19 +130,9 @@ export function PluginManagementCard({plugin, updatePlugin}: { function togglePluginEnabled() { if (isDisabled(plugin.state)) { - PluginManagementEndpoint.enablePlugin(plugin.id).then(() => { - PluginManagementEndpoint.getPlugin(plugin.id).then((response) => { - if (response === undefined) return; - updatePlugin(response); - }); - }); + PluginEndpoint.enablePlugin(plugin.id); } else { - PluginManagementEndpoint.disablePlugin(plugin.id).then(() => { - PluginManagementEndpoint.getPlugin(plugin.id).then((response) => { - if (response === undefined) return; - updatePlugin(response); - }); - }); + PluginEndpoint.disablePlugin(plugin.id); } } @@ -195,7 +184,6 @@ export function PluginManagementCard({plugin, updatePlugin}: { diff --git a/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx b/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx index 8efa9b1..1649b70 100644 --- a/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx +++ b/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx @@ -1,53 +1,35 @@ -import React, {useEffect, useState} from "react"; +import React from "react"; import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react"; import {Form, Formik} from "formik"; -import {PluginConfigEndpoint, PluginManagementEndpoint} from "Frontend/generated/endpoints"; -import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto"; import PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement"; import Input from "Frontend/components/general/input/Input"; import PluginLogo from "Frontend/components/general/PluginLogo"; import Markdown from "react-markdown"; import remarkBreaks from "remark-breaks"; +import {PluginEndpoint} from "Frontend/generated/endpoints"; +import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto"; interface PluginDetailsModalProps { plugin: PluginDto; isOpen: boolean; onOpenChange: () => void; - updatePlugin: (plugin: PluginDto) => void; } -export default function PluginDetailsModal({plugin, isOpen, onOpenChange, updatePlugin}: PluginDetailsModalProps) { - const [pluginConfigMeta, setPluginConfigMeta] = useState<(PluginConfigElement)[]>(); - const [pluginConfig, setPluginConfig] = useState>(); - - useEffect(() => { - PluginConfigEndpoint.getConfigMetadata(plugin.id).then(response => { - if (response === undefined) return; - setPluginConfigMeta(response as PluginConfigElement[]); - }); - PluginConfigEndpoint.getConfig(plugin.id).then(response => { - if (response === undefined) return; - setPluginConfig(response as Record); - }); - }, []); - +export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) { async function saveConfig(values: Record) { - await PluginConfigEndpoint.setConfigEntries(plugin.id, values); + await PluginEndpoint.updateConfig(plugin.id, values); addToast({ title: "Configuration saved", description: `Configuration for plugin ${plugin.name} saved!`, color: "success" }); - let updatedPlugin = await PluginManagementEndpoint.getPlugin(plugin.id); - if (updatedPlugin === undefined) return; - updatePlugin(updatedPlugin); } return ( {(onClose) => ( - { await saveConfig(values); @@ -107,8 +89,8 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update

Configuration

- {(pluginConfigMeta && pluginConfigMeta.length > 0) ? - pluginConfigMeta.map((entry: PluginConfigElement) => ( + {(plugin.configMetadata && plugin.configMetadata.length > 0) ? + plugin.configMetadata.map((entry: PluginConfigElement) => ( )) : "This plugin has no configuration options." @@ -118,7 +100,7 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update - {(pluginConfigMeta && pluginConfigMeta?.length > 0) ? + {(plugin.configMetadata && plugin.configMetadata?.length > 0) ?