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) ?