mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Implement realtime UI for plugins
Refactor PluginEndpoint Switch from Set to List for a minor performance boost
This commit is contained in:
Generated
+10
@@ -54,6 +54,7 @@
|
|||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"swiper": "^11.2.6",
|
"swiper": "^11.2.6",
|
||||||
"valtio": "^2.1.5",
|
"valtio": "^2.1.5",
|
||||||
|
"valtio-reactive": "^0.1.2",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"swiper": "^11.2.6",
|
"swiper": "^11.2.6",
|
||||||
"valtio": "^2.1.5",
|
"valtio": "^2.1.5",
|
||||||
|
"valtio-reactive": "^0.1.2",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -138,7 +139,8 @@
|
|||||||
"react-player": "$react-player",
|
"react-player": "$react-player",
|
||||||
"react-markdown": "$react-markdown",
|
"react-markdown": "$react-markdown",
|
||||||
"remark-breaks": "$remark-breaks",
|
"remark-breaks": "$remark-breaks",
|
||||||
"valtio": "$valtio"
|
"valtio": "$valtio",
|
||||||
|
"valtio-reactive": "$valtio-reactive"
|
||||||
},
|
},
|
||||||
"vaadin": {
|
"vaadin": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -199,6 +201,6 @@
|
|||||||
"workbox-core": "7.3.0",
|
"workbox-core": "7.3.0",
|
||||||
"workbox-precaching": "7.3.0"
|
"workbox-precaching": "7.3.0"
|
||||||
},
|
},
|
||||||
"hash": "c57af53043f6c6a9f0b03c75c28c3fde0bbfd828f9ce0e179263959c61ec888d"
|
"hash": "dc682332ca36d64f455f6e13888e1ffcca97e888cbad8d356973e830f7463a10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
import React from "react";
|
import React, {useEffect} from "react";
|
||||||
import {Divider} from "@heroui/react";
|
|
||||||
import {PluginManagementSection} from "Frontend/components/general/PluginManagementSection";
|
import {PluginManagementSection} from "Frontend/components/general/PluginManagementSection";
|
||||||
|
import {initializePluginState, pluginState} from "Frontend/state/PluginState";
|
||||||
|
import {useSnapshot} from "valtio/react";
|
||||||
|
|
||||||
export default function PluginManagement() {
|
export default function PluginManagement() {
|
||||||
|
|
||||||
// Defined manually for now to control the layout (order of categories)
|
// Defined manually for now to control the layout (order of categories)
|
||||||
const pluginTypes = ["GameMetadataProvider", "DownloadProvider"];
|
const pluginTypes = ["GameMetadataProvider", "DownloadProvider"];
|
||||||
|
|
||||||
return (
|
const state = useSnapshot(pluginState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializePluginState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return state.isLoaded && (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||||
<h2 className="text-2xl font-bold">Plugins</h2>
|
<h2 className="text-2xl font-bold">Plugins</h2>
|
||||||
</div>
|
</div>
|
||||||
<Divider className="mb-4"/>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
{pluginTypes.map(type =>
|
{pluginTypes.map(type =>
|
||||||
<PluginManagementSection key={type} pluginType={type}/>
|
// @ts-ignore
|
||||||
|
<PluginManagementSection key={type} type={type} plugins={state.pluginsByType[type]}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {Form, Formik} from "formik";
|
|||||||
import {Button, Skeleton} from "@heroui/react";
|
import {Button, Skeleton} from "@heroui/react";
|
||||||
import {Check, Info} from "@phosphor-icons/react";
|
import {Check, Info} from "@phosphor-icons/react";
|
||||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
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";
|
import {useSnapshot} from "valtio/react";
|
||||||
|
|
||||||
export default function withConfigPage(WrappedComponent: React.ComponentType<any>, title: String, validationSchema?: any) {
|
export default function withConfigPage(WrappedComponent: React.ComponentType<any>, title: String, validationSchema?: any) {
|
||||||
@@ -16,7 +16,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
|||||||
const state = useSnapshot(configState);
|
const state = useSnapshot(configState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeConfig();
|
initializeConfigState();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,14 +26,14 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
|||||||
}, [configSaved])
|
}, [configSaved])
|
||||||
|
|
||||||
async function handleSubmit(values: NestedConfig): Promise<void> {
|
async function handleSubmit(values: NestedConfig): Promise<void> {
|
||||||
const changed = getChangedValues(state.configNested, values);
|
const changed = getChangedValues(state.config, values);
|
||||||
await ConfigEndpoint.update({updates: changed});
|
await ConfigEndpoint.update({updates: changed});
|
||||||
setConfigSaved(true);
|
setConfigSaved(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig(key: string): ConfigEntryDto | undefined {
|
function getConfig(key: string): ConfigEntryDto | undefined {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return state.configEntries[key];
|
return state.state[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChangedValues(initial: NestedConfig, current: NestedConfig): Record<string, any> {
|
function getChangedValues(initial: NestedConfig, current: NestedConfig): Record<string, any> {
|
||||||
@@ -86,7 +86,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
|||||||
<>
|
<>
|
||||||
{state.isLoaded ?
|
{state.isLoaded ?
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={state.configNested}
|
initialValues={state.config}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
enableReinitialize={true}
|
enableReinitialize={true}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Plug} from "@phosphor-icons/react";
|
import {Plug} from "@phosphor-icons/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
|
|
||||||
import {Image} from "@heroui/react";
|
import {Image} from "@heroui/react";
|
||||||
|
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||||
|
|
||||||
interface PluginLogoProps {
|
interface PluginLogoProps {
|
||||||
plugin: PluginDto;
|
plugin: PluginDto;
|
||||||
|
|||||||
@@ -1,41 +1,23 @@
|
|||||||
import {Button, Tooltip, useDisclosure} from "@heroui/react";
|
import {Button, Tooltip, useDisclosure} from "@heroui/react";
|
||||||
import {ListNumbers} from "@phosphor-icons/react";
|
import {ListNumbers} from "@phosphor-icons/react";
|
||||||
import {PluginManagementCard} from "Frontend/components/general/cards/PluginManagementCard";
|
import {PluginManagementCard} from "Frontend/components/general/cards/PluginManagementCard";
|
||||||
import React, {useEffect, useState} from "react";
|
import React from "react";
|
||||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
|
|
||||||
import {PluginManagementEndpoint} from "Frontend/generated/endpoints";
|
|
||||||
import PluginPrioritiesModal from "Frontend/components/general/modals/PluginPrioritiesModal";
|
import PluginPrioritiesModal from "Frontend/components/general/modals/PluginPrioritiesModal";
|
||||||
import {camelCaseToTitle} from "Frontend/util/utils";
|
import {camelCaseToTitle} from "Frontend/util/utils";
|
||||||
|
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||||
|
|
||||||
interface PluginManagementSectionProps {
|
interface PluginManagementSectionProps {
|
||||||
pluginType: string;
|
type: string;
|
||||||
|
plugins: PluginDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PluginManagementSection({pluginType}: PluginManagementSectionProps) {
|
export function PluginManagementSection({type, plugins}: PluginManagementSectionProps) {
|
||||||
const [plugins, setPlugins] = useState<PluginDto[]>([]);
|
|
||||||
const pluginPrioritiesModal = useDisclosure();
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-row flex-grow justify-between">
|
<div className="flex flex-row flex-grow justify-between">
|
||||||
<h2 className="text-xl font-bold">{camelCaseToTitle(pluginType)}</h2>
|
<h2 className="text-xl font-bold">{camelCaseToTitle(type)}</h2>
|
||||||
|
|
||||||
<Tooltip color="foreground" placement="left" content="Change plugin order">
|
<Tooltip color="foreground" placement="left" content="Change plugin order">
|
||||||
<Button isIconOnly variant="flat" onPress={pluginPrioritiesModal.onOpen}>
|
<Button isIconOnly variant="flat" onPress={pluginPrioritiesModal.onOpen}>
|
||||||
@@ -45,9 +27,8 @@ export function PluginManagementSection({pluginType}: PluginManagementSectionPro
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-300px gap-4">
|
<div className="grid grid-cols-300px gap-4">
|
||||||
{plugins.map((plugin) => <PluginManagementCard plugin={plugin}
|
{plugins.map((plugin) =>
|
||||||
updatePlugin={updatePlugin}
|
<PluginManagementCard plugin={plugin} key={plugin.id}/>
|
||||||
key={plugin.name}/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import {
|
|||||||
WarningCircle,
|
WarningCircle,
|
||||||
XCircle
|
XCircle
|
||||||
} from "@phosphor-icons/react";
|
} 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 PluginState from "Frontend/generated/org/pf4j/PluginState";
|
||||||
import React, {ReactNode, useEffect, useState} from "react";
|
import React, {ReactNode, useEffect, useState} from "react";
|
||||||
import PluginDetailsModal from "Frontend/components/general/modals/PluginDetailsModal";
|
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 PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel";
|
||||||
import PluginConfigValidationResult
|
import PluginConfigValidationResult
|
||||||
from "Frontend/generated/de/grimsi/gameyfin/core/plugins/config/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}: {
|
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
||||||
plugin: PluginDto,
|
|
||||||
updatePlugin: (plugin: PluginDto) => void
|
|
||||||
}) {
|
|
||||||
const pluginDetailsModal = useDisclosure();
|
const pluginDetailsModal = useDisclosure();
|
||||||
const [configValidationResult, setConfigValidationResult] = useState<PluginConfigValidationResult | undefined>(undefined);
|
const [configValidationResult, setConfigValidationResult] = useState<PluginConfigValidationResult | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
PluginManagementEndpoint.validatePluginConfig(plugin.id).then((response: PluginConfigValidationResult | undefined) => {
|
PluginEndpoint.validatePluginConfig(plugin.id).then((response: PluginConfigValidationResult | undefined) => {
|
||||||
if (response === undefined) return;
|
if (response === undefined) return;
|
||||||
setConfigValidationResult(response);
|
setConfigValidationResult(response);
|
||||||
});
|
});
|
||||||
@@ -55,6 +52,7 @@ export function PluginManagementCard({plugin, updatePlugin}: {
|
|||||||
case PluginState.DISABLED:
|
case PluginState.DISABLED:
|
||||||
return "warning";
|
return "warning";
|
||||||
case PluginState.FAILED:
|
case PluginState.FAILED:
|
||||||
|
case PluginState.STOPPED:
|
||||||
return "danger";
|
return "danger";
|
||||||
default:
|
default:
|
||||||
return "default";
|
return "default";
|
||||||
@@ -67,6 +65,7 @@ export function PluginManagementCard({plugin, updatePlugin}: {
|
|||||||
return <PlayCircle/>;
|
return <PlayCircle/>;
|
||||||
case PluginState.DISABLED:
|
case PluginState.DISABLED:
|
||||||
return <PauseCircle/>;
|
return <PauseCircle/>;
|
||||||
|
case PluginState.STOPPED:
|
||||||
case PluginState.FAILED:
|
case PluginState.FAILED:
|
||||||
return <StopCircle/>;
|
return <StopCircle/>;
|
||||||
case PluginState.UNLOADED:
|
case PluginState.UNLOADED:
|
||||||
@@ -131,19 +130,9 @@ export function PluginManagementCard({plugin, updatePlugin}: {
|
|||||||
|
|
||||||
function togglePluginEnabled() {
|
function togglePluginEnabled() {
|
||||||
if (isDisabled(plugin.state)) {
|
if (isDisabled(plugin.state)) {
|
||||||
PluginManagementEndpoint.enablePlugin(plugin.id).then(() => {
|
PluginEndpoint.enablePlugin(plugin.id);
|
||||||
PluginManagementEndpoint.getPlugin(plugin.id).then((response) => {
|
|
||||||
if (response === undefined) return;
|
|
||||||
updatePlugin(response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
PluginManagementEndpoint.disablePlugin(plugin.id).then(() => {
|
PluginEndpoint.disablePlugin(plugin.id);
|
||||||
PluginManagementEndpoint.getPlugin(plugin.id).then((response) => {
|
|
||||||
if (response === undefined) return;
|
|
||||||
updatePlugin(response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +184,6 @@ export function PluginManagementCard({plugin, updatePlugin}: {
|
|||||||
<PluginDetailsModal plugin={plugin}
|
<PluginDetailsModal plugin={plugin}
|
||||||
isOpen={pluginDetailsModal.isOpen}
|
isOpen={pluginDetailsModal.isOpen}
|
||||||
onOpenChange={pluginDetailsModal.onOpenChange}
|
onOpenChange={pluginDetailsModal.onOpenChange}
|
||||||
updatePlugin={updatePlugin}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
||||||
import {Form, Formik} from "formik";
|
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 PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement";
|
||||||
import Input from "Frontend/components/general/input/Input";
|
import Input from "Frontend/components/general/input/Input";
|
||||||
import PluginLogo from "Frontend/components/general/PluginLogo";
|
import PluginLogo from "Frontend/components/general/PluginLogo";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import remarkBreaks from "remark-breaks";
|
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 {
|
interface PluginDetailsModalProps {
|
||||||
plugin: PluginDto;
|
plugin: PluginDto;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: () => void;
|
onOpenChange: () => void;
|
||||||
updatePlugin: (plugin: PluginDto) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PluginDetailsModal({plugin, isOpen, onOpenChange, updatePlugin}: PluginDetailsModalProps) {
|
export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) {
|
||||||
const [pluginConfigMeta, setPluginConfigMeta] = useState<(PluginConfigElement)[]>();
|
|
||||||
const [pluginConfig, setPluginConfig] = useState<Record<string, string>>();
|
|
||||||
|
|
||||||
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<string, string>);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function saveConfig(values: Record<string, string>) {
|
async function saveConfig(values: Record<string, string>) {
|
||||||
await PluginConfigEndpoint.setConfigEntries(plugin.id, values);
|
await PluginEndpoint.updateConfig(plugin.id, values);
|
||||||
addToast({
|
addToast({
|
||||||
title: "Configuration saved",
|
title: "Configuration saved",
|
||||||
description: `Configuration for plugin ${plugin.name} saved!`,
|
description: `Configuration for plugin ${plugin.name} saved!`,
|
||||||
color: "success"
|
color: "success"
|
||||||
});
|
});
|
||||||
let updatedPlugin = await PluginManagementEndpoint.getPlugin(plugin.id);
|
|
||||||
if (updatedPlugin === undefined) return;
|
|
||||||
updatePlugin(updatedPlugin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<Formik initialValues={pluginConfig}
|
<Formik initialValues={plugin.config}
|
||||||
enableReinitialize={true}
|
enableReinitialize={true}
|
||||||
onSubmit={async (values: any) => {
|
onSubmit={async (values: any) => {
|
||||||
await saveConfig(values);
|
await saveConfig(values);
|
||||||
@@ -107,8 +89,8 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 className="text-l font-bold mt-4">Configuration</h4>
|
<h4 className="text-l font-bold mt-4">Configuration</h4>
|
||||||
{(pluginConfigMeta && pluginConfigMeta.length > 0) ?
|
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
||||||
pluginConfigMeta.map((entry: PluginConfigElement) => (
|
plugin.configMetadata.map((entry: PluginConfigElement) => (
|
||||||
<Input key={entry.key} name={entry.key} label={entry.name}
|
<Input key={entry.key} name={entry.key} label={entry.name}
|
||||||
type={entry.secret ? "password" : "text"}/>
|
type={entry.secret ? "password" : "text"}/>
|
||||||
)) : "This plugin has no configuration options."
|
)) : "This plugin has no configuration options."
|
||||||
@@ -118,7 +100,7 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
|
|||||||
<Button variant="light" onPress={onClose}>
|
<Button variant="light" onPress={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{(pluginConfigMeta && pluginConfigMeta?.length > 0) ?
|
{(plugin.configMetadata && plugin.configMetadata?.length > 0) ?
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {addToast, Button, Chip, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
import {addToast, Button, Chip, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
||||||
import {PluginManagementEndpoint} from "Frontend/generated/endpoints";
|
|
||||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
|
|
||||||
import {ListBox, ListBoxItem, useDragAndDrop} from "react-aria-components";
|
import {ListBox, ListBoxItem, useDragAndDrop} from "react-aria-components";
|
||||||
import {CaretUpDown} from "@phosphor-icons/react";
|
import {CaretUpDown} from "@phosphor-icons/react";
|
||||||
import {useListData} from "@react-stately/data";
|
import {useListData} from "@react-stately/data";
|
||||||
|
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||||
|
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
|
||||||
interface PluginPrioritiesModalProps {
|
interface PluginPrioritiesModalProps {
|
||||||
plugins: PluginDto[];
|
plugins: PluginDto[];
|
||||||
@@ -51,11 +51,11 @@ export default function PluginPrioritiesModal({plugins, isOpen, onOpenChange}: P
|
|||||||
async function setPluginPriorities(onClose: () => void) {
|
async function setPluginPriorities(onClose: () => void) {
|
||||||
try {
|
try {
|
||||||
const prioritiesMap = generatePrioritiesMap();
|
const prioritiesMap = generatePrioritiesMap();
|
||||||
await PluginManagementEndpoint.setPluginPriorities(prioritiesMap);
|
await PluginEndpoint.setPluginPriorities(prioritiesMap);
|
||||||
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "Plugin order updated",
|
title: "Plugin order updated",
|
||||||
description: "Plugin order have been updated successfully.",
|
description: "Plugin order has been updated successfully.",
|
||||||
color: "success"
|
color: "success"
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -7,35 +7,35 @@ import {Subscription} from "@vaadin/hilla-frontend";
|
|||||||
type ConfigState = {
|
type ConfigState = {
|
||||||
subscription?: Subscription<ConfigUpdateDto>;
|
subscription?: Subscription<ConfigUpdateDto>;
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
configEntries: Record<string, ConfigEntryDto>;
|
state: Record<string, ConfigEntryDto>;
|
||||||
configNested: NestedConfig;
|
config: NestedConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const configState = proxy<ConfigState>({
|
export const configState = proxy<ConfigState>({
|
||||||
get isLoaded() {
|
get isLoaded() {
|
||||||
return this.subscription != null;
|
return this.subscription != null;
|
||||||
},
|
},
|
||||||
configEntries: {},
|
state: {},
|
||||||
get configNested() {
|
get config() {
|
||||||
return toNestedConfig(Object.values(this.configEntries));
|
return toNestedConfig(Object.values(this.state));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Subscribe to and process state updates from backend **/
|
/** Subscribe to and process state updates from backend **/
|
||||||
export async function initializeConfig() {
|
export async function initializeConfigState() {
|
||||||
if (configState.isLoaded) return;
|
if (configState.isLoaded) return;
|
||||||
|
|
||||||
// Fetch initial configuration data
|
// Fetch initial configuration data
|
||||||
const initialEntries = await ConfigEndpoint.getAll();
|
const initialEntries = await ConfigEndpoint.getAll();
|
||||||
initialEntries.forEach((entry) => {
|
initialEntries.forEach((entry) => {
|
||||||
configState.configEntries[entry.key] = entry;
|
configState.state[entry.key] = entry;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to real-time updates
|
// Subscribe to real-time updates
|
||||||
configState.subscription = ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
|
configState.subscription = ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
|
||||||
Object.entries(updateDto.updates).forEach(([key, value]) => {
|
Object.entries(updateDto.updates).forEach(([key, value]) => {
|
||||||
if (configState.configEntries[key]) {
|
if (configState.state[key]) {
|
||||||
configState.configEntries[key].value = value;
|
configState.state[key].value = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {Subscription} from "@vaadin/hilla-frontend";
|
||||||
|
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||||
|
import PluginUpdateDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginUpdateDto";
|
||||||
|
import {proxy} from "valtio/index";
|
||||||
|
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
|
||||||
|
type PluginState = {
|
||||||
|
subscription?: Subscription<PluginUpdateDto>;
|
||||||
|
isLoaded: boolean;
|
||||||
|
state: Record<string, PluginDto>;
|
||||||
|
plugins: PluginDto[];
|
||||||
|
pluginsByType: Record<string, PluginDto[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pluginState = proxy<PluginState>({
|
||||||
|
get isLoaded() {
|
||||||
|
return this.subscription != null;
|
||||||
|
},
|
||||||
|
state: {},
|
||||||
|
get plugins() {
|
||||||
|
return Object.values<PluginDto>(this.state);
|
||||||
|
},
|
||||||
|
get pluginsByType() {
|
||||||
|
return groupPluginsByType(this.state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Subscribe to and process state updates from backend **/
|
||||||
|
export async function initializePluginState() {
|
||||||
|
if (pluginState.isLoaded) return;
|
||||||
|
|
||||||
|
// Fetch initial plugin list
|
||||||
|
const initialEntries = await PluginEndpoint.getAll();
|
||||||
|
initialEntries.forEach((plugin: PluginDto) => {
|
||||||
|
pluginState.state[plugin.id] = plugin;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to real-time updates
|
||||||
|
pluginState.subscription = PluginEndpoint.subscribe().onNext((updateDto: PluginUpdateDto) => {
|
||||||
|
// Make sure the plugin exists in the state
|
||||||
|
if (pluginState.state[updateDto.id]) {
|
||||||
|
// Update the existing plugin by merging the new data using destructuring
|
||||||
|
pluginState.state[updateDto.id] = {
|
||||||
|
...pluginState.state[updateDto.id],
|
||||||
|
...updateDto
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Computed **/
|
||||||
|
|
||||||
|
function groupPluginsByType(pluginsMap: Record<string, PluginDto>): Record<string, PluginDto[]> {
|
||||||
|
const pluginsByType: Record<string, PluginDto[]> = {};
|
||||||
|
|
||||||
|
// Convert map to array of plugins
|
||||||
|
const plugins = Object.values(pluginsMap);
|
||||||
|
|
||||||
|
// Iterate through each plugin
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
// Each plugin can have multiple types
|
||||||
|
for (const type of plugin.types) {
|
||||||
|
// Initialize array for this type if it doesn't exist
|
||||||
|
if (!pluginsByType[type]) {
|
||||||
|
pluginsByType[type] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add plugin to the appropriate type array
|
||||||
|
pluginsByType[type].push(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginsByType;
|
||||||
|
}
|
||||||
@@ -10,42 +10,31 @@ import jakarta.annotation.security.RolesAllowed
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
class ConfigEndpoint(
|
class ConfigEndpoint(
|
||||||
private val config: ConfigService
|
private val configService: ConfigService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** CRUD endpoints for admins **/
|
/** CRUD endpoints for admins **/
|
||||||
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
|
|
||||||
|
|
||||||
fun getAll(): List<ConfigEntryDto> {
|
|
||||||
return config.getAll(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun subscribe(): Flux<ConfigUpdateDto> {
|
fun subscribe(): Flux<ConfigUpdateDto> {
|
||||||
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
|
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
|
||||||
return if (user.isAdmin()) configUpdates.asFlux()
|
return if (user.isAdmin()) configService.subscribe()
|
||||||
else Flux.empty()
|
else Flux.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(update: ConfigUpdateDto) {
|
fun getAll(): List<ConfigEntryDto> = configService.getAll()
|
||||||
config.update(update.updates)
|
|
||||||
configUpdates.tryEmitNext(update)
|
fun update(update: ConfigUpdateDto) = configService.update(update)
|
||||||
}
|
|
||||||
|
|
||||||
/** Specific read-only endpoint for all users **/
|
/** Specific read-only endpoint for all users **/
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun isSsoEnabled(): Boolean? {
|
fun isSsoEnabled(): Boolean? = configService.get(ConfigProperties.SSO.OIDC.Enabled)
|
||||||
return config.get(ConfigProperties.SSO.OIDC.Enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun getLogoutUrl(): String? {
|
fun getLogoutUrl(): String? = configService.get(ConfigProperties.SSO.OIDC.LogoutUrl)
|
||||||
return config.get(ConfigProperties.SSO.OIDC.LogoutUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package de.grimsi.gameyfin.config
|
package de.grimsi.gameyfin.config
|
||||||
|
|
||||||
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
||||||
|
import de.grimsi.gameyfin.config.dto.ConfigUpdateDto
|
||||||
import de.grimsi.gameyfin.config.entities.ConfigEntry
|
import de.grimsi.gameyfin.config.entities.ConfigEntry
|
||||||
import de.grimsi.gameyfin.config.persistence.ConfigRepository
|
import de.grimsi.gameyfin.config.persistence.ConfigRepository
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
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.Sinks
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -14,6 +17,11 @@ class ConfigService(
|
|||||||
) {
|
) {
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
|
||||||
|
|
||||||
|
fun subscribe(): Flux<ConfigUpdateDto> {
|
||||||
|
return configUpdates.asFlux()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current value of a config property in a type-safe way.
|
* Get the current value of a config property in a type-safe way.
|
||||||
@@ -52,21 +60,16 @@ class ConfigService(
|
|||||||
/**
|
/**
|
||||||
* Get all known config values.
|
* Get all known config values.
|
||||||
*
|
*
|
||||||
* @param prefix: Optional prefix to filter the config values
|
|
||||||
* @return A map of all config values
|
* @return A map of all config values
|
||||||
*/
|
*/
|
||||||
fun getAll(prefix: String?): List<ConfigEntryDto> {
|
fun getAll(): List<ConfigEntryDto> {
|
||||||
|
|
||||||
log.debug { "Getting all config values for prefix '$prefix'" }
|
log.debug { "Getting all config values" }
|
||||||
|
|
||||||
var configProperties = ConfigProperties::class.sealedSubclasses.flatMap { subclass ->
|
val configProperties = ConfigProperties::class.sealedSubclasses.flatMap { subclass ->
|
||||||
subclass.objectInstance?.let { listOf(it) } ?: listOf()
|
subclass.objectInstance?.let { listOf(it) } ?: listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefix != null) {
|
|
||||||
configProperties = configProperties.filter { it.key.startsWith(prefix) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return configProperties.map { configProperty ->
|
return configProperties.map { configProperty ->
|
||||||
ConfigEntryDto(
|
ConfigEntryDto(
|
||||||
key = configProperty.key,
|
key = configProperty.key,
|
||||||
@@ -115,16 +118,17 @@ class ConfigService(
|
|||||||
* Set multiple config values at once.
|
* Set multiple config values at once.
|
||||||
* Configs with a null value will be deleted.
|
* Configs with a null value will be deleted.
|
||||||
*
|
*
|
||||||
* @param updates: A map of key-value pairs to set
|
* @param update: A [ConfigUpdateDto] containing a map of key-value pairs to set
|
||||||
*/
|
*/
|
||||||
fun update(updates: Map<String, Serializable?>) {
|
fun update(update: ConfigUpdateDto) {
|
||||||
updates.forEach { (key, value) ->
|
update.updates.forEach { (key, value) ->
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
delete(key)
|
delete(key)
|
||||||
} else {
|
} else {
|
||||||
set(key, value)
|
set(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
configUpdates.tryEmitNext(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SetupDataLoader(
|
|||||||
email = "admin@gameyfin.org",
|
email = "admin@gameyfin.org",
|
||||||
emailConfirmed = true,
|
emailConfirmed = true,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
roles = setOf(Role.SUPERADMIN)
|
roles = listOf(Role.SUPERADMIN)
|
||||||
)
|
)
|
||||||
|
|
||||||
registerUserIfNotFound(superadmin)
|
registerUserIfNotFound(superadmin)
|
||||||
@@ -56,7 +56,7 @@ class SetupDataLoader(
|
|||||||
email = "user@gameyfin.org",
|
email = "user@gameyfin.org",
|
||||||
emailConfirmed = true,
|
emailConfirmed = true,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
roles = setOf(Role.USER)
|
roles = listOf(Role.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
registerUserIfNotFound(user)
|
registerUserIfNotFound(user)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.core.filesystem
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
data class FilesystemScanResult(
|
data class FilesystemScanResult(
|
||||||
val newPaths: Set<Path>,
|
val newPaths: List<Path>,
|
||||||
val removedGamePaths: Set<Path>,
|
val removedGamePaths: List<Path>,
|
||||||
val removedUnmatchedPaths: Set<Path>
|
val removedUnmatchedPaths: List<Path>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -116,18 +116,18 @@ class FilesystemService(
|
|||||||
val newPaths = currentFilesystemPaths.filter { path ->
|
val newPaths = currentFilesystemPaths.filter { path ->
|
||||||
val isInLibrary = allCurrentLibraryPaths.any { it == path }
|
val isInLibrary = allCurrentLibraryPaths.any { it == path }
|
||||||
!isInLibrary
|
!isInLibrary
|
||||||
}.toSet()
|
}
|
||||||
|
|
||||||
//Get all paths that are in the library (either as game or as unmatched path), but not on the filesystem
|
//Get all paths that are in the library (either as game or as unmatched path), but not on the filesystem
|
||||||
val removedGamePaths = currentLibraryGamePaths.filter { path ->
|
val removedGamePaths = currentLibraryGamePaths.filter { path ->
|
||||||
val isOnFilesystem = currentFilesystemPaths.any { it == path }
|
val isOnFilesystem = currentFilesystemPaths.any { it == path }
|
||||||
!isOnFilesystem
|
!isOnFilesystem
|
||||||
}.toSet()
|
}
|
||||||
|
|
||||||
val removedUnmatchedPaths = currentLibraryUnmatchedPaths.filter { path ->
|
val removedUnmatchedPaths = currentLibraryUnmatchedPaths.filter { path ->
|
||||||
val isOnFilesystem = currentFilesystemPaths.any { it == path }
|
val isOnFilesystem = currentFilesystemPaths.any { it == path }
|
||||||
!isOnFilesystem
|
!isOnFilesystem
|
||||||
}.toSet()
|
}
|
||||||
|
|
||||||
return FilesystemScanResult(
|
return FilesystemScanResult(
|
||||||
newPaths = newPaths,
|
newPaths = newPaths,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.grimsi.gameyfin.core.plugins
|
||||||
|
|
||||||
|
import com.vaadin.hilla.Endpoint
|
||||||
|
import de.grimsi.gameyfin.core.Role
|
||||||
|
import de.grimsi.gameyfin.core.plugins.config.PluginConfigValidationResult
|
||||||
|
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
||||||
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementService
|
||||||
|
import de.grimsi.gameyfin.users.util.isAdmin
|
||||||
|
import jakarta.annotation.security.PermitAll
|
||||||
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
|
@Endpoint
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
|
class PluginEndpoint(
|
||||||
|
private val pluginManagementService: PluginManagementService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PermitAll
|
||||||
|
fun subscribe(): Flux<PluginUpdateDto> {
|
||||||
|
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
|
||||||
|
return if (user.isAdmin()) pluginManagementService.subscribe()
|
||||||
|
else Flux.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAll() = pluginManagementService.getAll()
|
||||||
|
|
||||||
|
fun enablePlugin(pluginId: String) = pluginManagementService.enablePlugin(pluginId)
|
||||||
|
|
||||||
|
fun disablePlugin(pluginId: String) = pluginManagementService.disablePlugin(pluginId)
|
||||||
|
|
||||||
|
fun setPluginPriorities(pluginPriorities: Map<String, Int>) =
|
||||||
|
pluginManagementService.setPluginPriorities(pluginPriorities)
|
||||||
|
|
||||||
|
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult =
|
||||||
|
pluginManagementService.validatePluginConfig(pluginId)
|
||||||
|
|
||||||
|
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) =
|
||||||
|
pluginManagementService.updateConfig(pluginId, updatedConfig)
|
||||||
|
}
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.config
|
|
||||||
|
|
||||||
import com.vaadin.hilla.Endpoint
|
|
||||||
import de.grimsi.gameyfin.core.Role
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
|
||||||
import jakarta.annotation.security.RolesAllowed
|
|
||||||
|
|
||||||
@Endpoint
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
|
||||||
class PluginConfigEndpoint(
|
|
||||||
private val pluginConfigService: PluginConfigService
|
|
||||||
) {
|
|
||||||
fun getConfigMetadata(pluginId: String): List<PluginConfigElement> {
|
|
||||||
return pluginConfigService.getConfigMetadata(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getConfig(pluginId: String): Map<String, String?> {
|
|
||||||
return pluginConfigService.getConfig(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setConfigEntries(pluginId: String, config: Map<String, String>) {
|
|
||||||
pluginConfigService.setConfigEntries(pluginId, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setConfigEntry(pluginId: String, key: String, value: String) {
|
|
||||||
pluginConfigService.setConfigEntry(pluginId, key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-21
@@ -4,6 +4,7 @@ import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
|
|||||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.pf4j.PluginWrapper
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -14,36 +15,22 @@ class PluginConfigService(
|
|||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun getConfigMetadata(pluginId: String): List<PluginConfigElement> {
|
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigElement> {
|
||||||
log.debug { "Getting config metadata for plugin $pluginId" }
|
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
||||||
|
val plugin = pluginWrapper.plugin
|
||||||
val plugin = try {
|
|
||||||
pluginManager.getPlugin(pluginId).plugin
|
|
||||||
} catch (_: NoClassDefFoundError) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin !is Configurable) return emptyList()
|
if (plugin !is Configurable) return emptyList()
|
||||||
|
|
||||||
return plugin.configMetadata
|
return plugin.configMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConfig(pluginId: String): Map<String, String?> {
|
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||||
log.debug { "Getting config for plugin $pluginId" }
|
log.debug { "Getting config for plugin ${pluginWrapper.pluginId}" }
|
||||||
return pluginConfigRepository.findAllById_PluginId(pluginId).associate { it.id.key to it.value }
|
return pluginConfigRepository.findAllById_PluginId(pluginWrapper.pluginId).associate { it.id.key to it.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setConfigEntries(pluginId: String, config: Map<String, String>) {
|
fun updateConfig(pluginId: String, config: Map<String, String>) {
|
||||||
log.debug { "Setting config entries for plugin $pluginId" }
|
log.debug { "Setting config entries for plugin $pluginId" }
|
||||||
val entries = config.map { PluginConfigEntry(PluginConfigEntryKey(pluginId, it.key), it.value) }
|
val entries = config.map { PluginConfigEntry(PluginConfigEntryKey(pluginId, it.key), it.value) }
|
||||||
pluginConfigRepository.saveAll(entries)
|
pluginConfigRepository.saveAll(entries)
|
||||||
pluginManager.restart(pluginId)
|
pluginManager.restart(pluginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setConfigEntry(pluginId: String, key: String, value: String) {
|
|
||||||
log.debug { "Setting config entry $key for plugin $pluginId" }
|
|
||||||
val entry = PluginConfigEntry(PluginConfigEntryKey(pluginId, key), value)
|
|
||||||
pluginConfigRepository.save(entry)
|
|
||||||
pluginManager.restart(pluginId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+6
-1
@@ -1,9 +1,12 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.management
|
package de.grimsi.gameyfin.core.plugins.dto
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.core.plugins.management.PluginTrustLevel
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||||
import org.pf4j.PluginState
|
import org.pf4j.PluginState
|
||||||
|
|
||||||
data class PluginDto(
|
data class PluginDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
val types: List<String>,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val shortDescription: String? = null,
|
val shortDescription: String? = null,
|
||||||
@@ -13,6 +16,8 @@ data class PluginDto(
|
|||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
val hasLogo: Boolean,
|
val hasLogo: Boolean,
|
||||||
val state: PluginState,
|
val state: PluginState,
|
||||||
|
val configMetadata: List<PluginConfigElement>? = null,
|
||||||
|
val config: Map<String, String?>? = null,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
val trustLevel: PluginTrustLevel,
|
val trustLevel: PluginTrustLevel,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.grimsi.gameyfin.core.plugins.dto
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import org.pf4j.PluginState
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class PluginUpdateDto(
|
||||||
|
val id: String,
|
||||||
|
val state: PluginState? = null,
|
||||||
|
val config: Map<String, String?>? = null,
|
||||||
|
val priority: Int? = null
|
||||||
|
)
|
||||||
+2
-4
@@ -181,19 +181,17 @@ class GameyfinPluginManager(
|
|||||||
return PluginConfigValidationResult.INVALID
|
return PluginConfigValidationResult.INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionTypeClasses(pluginId: String): Set<Class<ExtensionPoint>> {
|
fun getExtensionTypeClasses(pluginId: String): List<Class<ExtensionPoint>> {
|
||||||
return getExtensionClasses(pluginId)
|
return getExtensionClasses(pluginId)
|
||||||
.flatMap { it.interfaces.toList() }
|
.flatMap { it.interfaces.toList() }
|
||||||
.filterIsInstance<Class<ExtensionPoint>>()
|
.filterIsInstance<Class<ExtensionPoint>>()
|
||||||
.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionTypes(pluginId: String): Set<String> {
|
fun getExtensionTypes(pluginId: String): List<String> {
|
||||||
return getExtensionClasses(pluginId)
|
return getExtensionClasses(pluginId)
|
||||||
.flatMap { it.interfaces.toList() }
|
.flatMap { it.interfaces.toList() }
|
||||||
.filterIsInstance<Class<ExtensionPoint>>()
|
.filterIsInstance<Class<ExtensionPoint>>()
|
||||||
.map { it.simpleName }
|
.map { it.simpleName }
|
||||||
.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configurePlugin(pluginWrapper: PluginWrapper) {
|
private fun configurePlugin(pluginWrapper: PluginWrapper) {
|
||||||
|
|||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.management
|
|
||||||
|
|
||||||
import com.vaadin.hilla.Endpoint
|
|
||||||
import de.grimsi.gameyfin.core.Role
|
|
||||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigValidationResult
|
|
||||||
import jakarta.annotation.security.RolesAllowed
|
|
||||||
|
|
||||||
@Endpoint
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
|
||||||
class PluginManagementEndpoint(
|
|
||||||
private val pluginManagementService: PluginManagementService
|
|
||||||
) {
|
|
||||||
fun getSupportedPluginTypes() = pluginManagementService.getSupportedPluginTypes()
|
|
||||||
|
|
||||||
fun getPlugins(type: String?) = pluginManagementService.getPluginDtos(type)
|
|
||||||
|
|
||||||
fun getPluginsMappedToTypes() = pluginManagementService.getPluginDtosMappedToTypes()
|
|
||||||
|
|
||||||
fun getPlugin(pluginId: String) = pluginManagementService.getPluginDto(pluginId)
|
|
||||||
|
|
||||||
fun startPlugin(pluginId: String) = pluginManagementService.startPlugin(pluginId)
|
|
||||||
|
|
||||||
fun stopPlugin(pluginId: String) = pluginManagementService.stopPlugin(pluginId)
|
|
||||||
|
|
||||||
fun restartPlugin(pluginId: String) = pluginManagementService.restartPlugin(pluginId)
|
|
||||||
|
|
||||||
fun enablePlugin(pluginId: String) = pluginManagementService.enablePlugin(pluginId)
|
|
||||||
|
|
||||||
fun disablePlugin(pluginId: String) = pluginManagementService.disablePlugin(pluginId)
|
|
||||||
|
|
||||||
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult =
|
|
||||||
pluginManagementService.validatePluginConfig(pluginId)
|
|
||||||
|
|
||||||
fun setPluginPriority(pluginId: String, priority: Int) =
|
|
||||||
pluginManagementService.setPluginPriority(pluginId, priority)
|
|
||||||
|
|
||||||
fun setPluginPriorities(pluginPriorities: Map<String, Int>) =
|
|
||||||
pluginManagementService.setPluginPriorities(pluginPriorities)
|
|
||||||
}
|
|
||||||
+26
-35
@@ -1,43 +1,43 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.management
|
package de.grimsi.gameyfin.core.plugins.management
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.core.plugins.config.PluginConfigService
|
||||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigValidationResult
|
import de.grimsi.gameyfin.core.plugins.config.PluginConfigValidationResult
|
||||||
|
import de.grimsi.gameyfin.core.plugins.dto.PluginDto
|
||||||
|
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
||||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||||
import org.pf4j.ExtensionPoint
|
import org.pf4j.ExtensionPoint
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
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.Sinks
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class PluginManagementService(
|
class PluginManagementService(
|
||||||
private val pluginManager: GameyfinPluginManager,
|
private val pluginManager: GameyfinPluginManager,
|
||||||
|
private val pluginConfigService: PluginConfigService,
|
||||||
private val pluginManagementRepository: PluginManagementRepository,
|
private val pluginManagementRepository: PluginManagementRepository,
|
||||||
) {
|
) {
|
||||||
|
private val pluginUpdates = Sinks.many().multicast().onBackpressureBuffer<PluginUpdateDto>()
|
||||||
|
|
||||||
fun getSupportedPluginTypes(): Set<String> {
|
init {
|
||||||
|
pluginManager.addPluginStateListener {
|
||||||
|
pluginUpdates.tryEmitNext(PluginUpdateDto(id = it.plugin.pluginId, state = it.pluginState))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun subscribe(): Flux<PluginUpdateDto> {
|
||||||
|
return pluginUpdates.asFlux()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSupportedPluginTypes(): List<String> {
|
||||||
return pluginManager.plugins
|
return pluginManager.plugins
|
||||||
.flatMap { pluginManager.getExtensionTypes(it.pluginId) }
|
.flatMap { pluginManager.getExtensionTypes(it.pluginId) }
|
||||||
.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPluginDtos(type: String?): Set<PluginDto> {
|
fun getAll(): List<PluginDto> {
|
||||||
return pluginManager.plugins
|
return pluginManager.plugins
|
||||||
.filter { type == null || type in pluginManager.getExtensionTypes(it.pluginId) }
|
|
||||||
.map { toDto(it) }
|
.map { toDto(it) }
|
||||||
.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPluginDtosMappedToTypes(): Map<String, List<PluginDto>> {
|
|
||||||
return pluginManager.plugins
|
|
||||||
.flatMap { plugin ->
|
|
||||||
val types = pluginManager.getExtensionTypes(plugin.pluginId)
|
|
||||||
types.map { it to toDto(plugin) }
|
|
||||||
}
|
|
||||||
.groupBy({ it.first }, { it.second })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPluginDto(pluginId: String): PluginDto {
|
|
||||||
val plugin = pluginManager.getPlugin(pluginId)
|
|
||||||
return toDto(plugin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPluginManagementEntry(pluginId: String): PluginManagementEntry {
|
fun getPluginManagementEntry(pluginId: String): PluginManagementEntry {
|
||||||
@@ -51,18 +51,6 @@ class PluginManagementService(
|
|||||||
?: throw IllegalArgumentException("Plugin with class $clazz not found")
|
?: throw IllegalArgumentException("Plugin with class $clazz not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPlugin(pluginId: String) {
|
|
||||||
pluginManager.startPlugin(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopPlugin(pluginId: String) {
|
|
||||||
pluginManager.stopPlugin(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restartPlugin(pluginId: String) {
|
|
||||||
pluginManager.restart(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enablePlugin(pluginId: String) {
|
fun enablePlugin(pluginId: String) {
|
||||||
pluginManager.enablePlugin(pluginId)
|
pluginManager.enablePlugin(pluginId)
|
||||||
}
|
}
|
||||||
@@ -75,10 +63,10 @@ class PluginManagementService(
|
|||||||
return pluginManager.validatePluginConfig(pluginId)
|
return pluginManager.validatePluginConfig(pluginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPluginPriority(pluginId: String, priority: Int) {
|
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) {
|
||||||
val pluginManagementEntry = getPluginManagementEntry(pluginId)
|
pluginConfigService.updateConfig(pluginId, updatedConfig)
|
||||||
pluginManagementEntry.priority = priority
|
val update = PluginUpdateDto(pluginId, config = updatedConfig)
|
||||||
pluginManagementRepository.save(pluginManagementEntry)
|
pluginUpdates.tryEmitNext(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPluginPriorities(pluginPriorities: Map<String, Int>) {
|
fun setPluginPriorities(pluginPriorities: Map<String, Int>) {
|
||||||
@@ -110,6 +98,7 @@ class PluginManagementService(
|
|||||||
|
|
||||||
return PluginDto(
|
return PluginDto(
|
||||||
id = descriptor.pluginId,
|
id = descriptor.pluginId,
|
||||||
|
types = pluginManager.getExtensionTypes(pluginWrapper.pluginId),
|
||||||
name = descriptor.pluginName,
|
name = descriptor.pluginName,
|
||||||
description = descriptor.pluginDescription,
|
description = descriptor.pluginDescription,
|
||||||
shortDescription = descriptor.pluginShortDescription,
|
shortDescription = descriptor.pluginShortDescription,
|
||||||
@@ -119,6 +108,8 @@ class PluginManagementService(
|
|||||||
url = descriptor.pluginUrl,
|
url = descriptor.pluginUrl,
|
||||||
hasLogo = hasLogo,
|
hasLogo = hasLogo,
|
||||||
state = pluginWrapper.pluginState,
|
state = pluginWrapper.pluginState,
|
||||||
|
configMetadata = pluginConfigService.getConfigMetadata(pluginWrapper),
|
||||||
|
config = pluginConfigService.getConfig(pluginWrapper),
|
||||||
priority = pluginManagementEntry.priority,
|
priority = pluginManagementEntry.priority,
|
||||||
trustLevel = pluginManagementEntry.trustLevel
|
trustLevel = pluginManagementEntry.trustLevel
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ class GameService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun create(games: Collection<Game>): Collection<Game> {
|
fun create(games: List<Game>): List<Game> {
|
||||||
val gamesToBePersisted = games.filter { it.id == null }
|
val gamesToBePersisted = games.filter { it.id == null }
|
||||||
|
|
||||||
gamesToBePersisted.forEach { game ->
|
gamesToBePersisted.forEach { game ->
|
||||||
game.publishers = game.publishers.map { companyService.createOrGet(it) }.toSet()
|
game.publishers = game.publishers.map { companyService.createOrGet(it) }
|
||||||
game.developers = game.developers.map { companyService.createOrGet(it) }.toSet()
|
game.developers = game.developers.map { companyService.createOrGet(it) }
|
||||||
game
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +87,11 @@ class GameService(
|
|||||||
return mergedGame
|
return mergedGame
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllByPaths(paths: Collection<String>): Collection<Game> {
|
fun getAllByPaths(paths: List<String>): List<Game> {
|
||||||
return gameRepository.findAllByPathIn(paths)
|
return gameRepository.findAllByPathIn(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllGames(): Collection<GameDto> {
|
fun getAllGames(): List<GameDto> {
|
||||||
val entities = gameRepository.findAll()
|
val entities = gameRepository.findAll()
|
||||||
return entities.map { it.toDto() }
|
return entities.map { it.toDto() }
|
||||||
}
|
}
|
||||||
@@ -231,58 +231,58 @@ class GameService(
|
|||||||
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) }.toSet()
|
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }
|
||||||
metadataMap["publishers"] = FieldMetadata(sourcePlugin)
|
metadataMap["publishers"] = FieldMetadata(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) }.toSet()
|
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }
|
||||||
metadataMap["developers"] = FieldMetadata(sourcePlugin)
|
metadataMap["developers"] = FieldMetadata(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
|
mergedGame.genres = genres.toList()
|
||||||
metadataMap["genres"] = FieldMetadata(sourcePlugin)
|
metadataMap["genres"] = FieldMetadata(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
|
mergedGame.themes = themes.toList()
|
||||||
metadataMap["themes"] = FieldMetadata(sourcePlugin)
|
metadataMap["themes"] = FieldMetadata(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
|
mergedGame.keywords = keywords.toList()
|
||||||
metadataMap["keywords"] = FieldMetadata(sourcePlugin)
|
metadataMap["keywords"] = FieldMetadata(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
|
mergedGame.features = features.toList()
|
||||||
metadataMap["features"] = FieldMetadata(sourcePlugin)
|
metadataMap["features"] = FieldMetadata(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
|
mergedGame.perspectives = perspectives.toList()
|
||||||
metadataMap["perspectives"] = FieldMetadata(sourcePlugin)
|
metadataMap["perspectives"] = FieldMetadata(sourcePlugin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
|
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
|
||||||
if (!metadataMap.containsKey("images")) {
|
if (!metadataMap.containsKey("images")) {
|
||||||
mergedGame.images = runBlocking {
|
mergedGame.images = runBlocking {
|
||||||
screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) }.toSet()
|
screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) }
|
||||||
}
|
}
|
||||||
metadataMap["images"] = FieldMetadata(sourcePlugin)
|
metadataMap["images"] = FieldMetadata(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
|
mergedGame.videoUrls = videoUrls.toList()
|
||||||
metadataMap["videoUrls"] = FieldMetadata(sourcePlugin)
|
metadataMap["videoUrls"] = FieldMetadata(sourcePlugin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,31 +49,31 @@ class Game(
|
|||||||
var criticRating: Int? = null,
|
var criticRating: Int? = null,
|
||||||
|
|
||||||
@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
|
@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
|
||||||
var publishers: Set<Company> = emptySet(),
|
var publishers: List<Company> = emptyList(),
|
||||||
|
|
||||||
@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
|
@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
|
||||||
var developers: Set<Company> = emptySet(),
|
var developers: List<Company> = emptyList(),
|
||||||
|
|
||||||
@ElementCollection(targetClass = Genre::class)
|
@ElementCollection(targetClass = Genre::class)
|
||||||
var genres: Set<Genre> = emptySet(),
|
var genres: List<Genre> = emptyList(),
|
||||||
|
|
||||||
@ElementCollection(targetClass = Theme::class)
|
@ElementCollection(targetClass = Theme::class)
|
||||||
var themes: Set<Theme> = emptySet(),
|
var themes: List<Theme> = emptyList(),
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
var keywords: Set<String> = emptySet(),
|
var keywords: List<String> = emptyList(),
|
||||||
|
|
||||||
@ElementCollection(targetClass = GameFeature::class)
|
@ElementCollection(targetClass = GameFeature::class)
|
||||||
var features: Set<GameFeature> = emptySet(),
|
var features: List<GameFeature> = emptyList(),
|
||||||
|
|
||||||
@ElementCollection(targetClass = PlayerPerspective::class)
|
@ElementCollection(targetClass = PlayerPerspective::class)
|
||||||
var perspectives: Set<PlayerPerspective>? = null,
|
var perspectives: List<PlayerPerspective>? = null,
|
||||||
|
|
||||||
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||||
var images: Set<Image> = emptySet(),
|
var images: List<Image> = emptyList(),
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
var videoUrls: Set<URI> = emptySet(),
|
var videoUrls: List<URI> = emptyList(),
|
||||||
|
|
||||||
@Column(unique = true)
|
@Column(unique = true)
|
||||||
val path: String,
|
val path: String,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository
|
|||||||
|
|
||||||
interface GameRepository : JpaRepository<Game, Long> {
|
interface GameRepository : JpaRepository<Game, Long> {
|
||||||
fun findByPath(path: String): Game?
|
fun findByPath(path: String): Game?
|
||||||
fun findAllByPathIn(paths: Collection<String>): Collection<Game>
|
fun findAllByPathIn(paths: List<String>): List<Game>
|
||||||
fun findByOrderByCreatedAtDesc(limit: Limit): List<Game>
|
fun findByOrderByCreatedAtDesc(limit: Limit): List<Game>
|
||||||
fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
|
fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,11 @@ class Library(
|
|||||||
var name: String,
|
var name: String,
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = [CascadeType.ALL])
|
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = [CascadeType.ALL])
|
||||||
var directories: MutableSet<DirectoryMapping> = HashSet(),
|
var directories: MutableList<DirectoryMapping> = ArrayList(),
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true)
|
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
var games: MutableSet<Game> = HashSet(),
|
var games: MutableList<Game> = ArrayList(),
|
||||||
|
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
var unmatchedPaths: MutableSet<String> = HashSet()
|
var unmatchedPaths: MutableList<String> = ArrayList()
|
||||||
)
|
)
|
||||||
@@ -3,8 +3,8 @@ package de.grimsi.gameyfin.libraries
|
|||||||
import de.grimsi.gameyfin.games.entities.Game
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
|
|
||||||
data class LibraryScanResult(
|
data class LibraryScanResult(
|
||||||
val libraries: Set<Library>,
|
val libraries: List<Library>,
|
||||||
val newGames: Set<Game>,
|
val newGames: List<Game>,
|
||||||
val removedGames: Set<Game>,
|
val removedGames: List<Game>,
|
||||||
val newUnmatchedPaths: Set<String>
|
val newUnmatchedPaths: List<String>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class LibraryService(
|
|||||||
libraryDto.directories?.let {
|
libraryDto.directories?.let {
|
||||||
existingLibrary.directories = it
|
existingLibrary.directories = it
|
||||||
.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
||||||
.toMutableSet()
|
.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val updatedLibrary = libraryRepository.save(existingLibrary)
|
val updatedLibrary = libraryRepository.save(existingLibrary)
|
||||||
@@ -271,10 +271,10 @@ class LibraryService(
|
|||||||
libraryRepository.save(library)
|
libraryRepository.save(library)
|
||||||
|
|
||||||
return LibraryScanResult(
|
return LibraryScanResult(
|
||||||
libraries = setOf(library),
|
libraries = listOf(library),
|
||||||
newGames = persistedGames.toSet(),
|
newGames = persistedGames,
|
||||||
removedGames = removedGames.toSet(),
|
removedGames = removedGames,
|
||||||
newUnmatchedPaths = newUnmatchedPaths
|
newUnmatchedPaths = newUnmatchedPaths.toList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ class LibraryService(
|
|||||||
name = library.name,
|
name = library.name,
|
||||||
directories = library.directories.map {
|
directories = library.directories.map {
|
||||||
DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath)
|
DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath)
|
||||||
}.toMutableSet()
|
}.toMutableList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@ package de.grimsi.gameyfin.libraries.dto
|
|||||||
data class LibraryUpdateDto(
|
data class LibraryUpdateDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val directories: Set<DirectoryMappingDto>? = null,
|
val directories: List<DirectoryMappingDto>? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ class MessageTemplateEndpoint(
|
|||||||
return messageTemplateService.getMessageTemplate(key)
|
return messageTemplateService.getMessageTemplate(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefaultPlaceholders(type: TemplateType): Set<String> {
|
fun getDefaultPlaceholders(type: TemplateType): List<String> {
|
||||||
return messageTemplateService.getDefaultTemplatePlaceholders(type).keys
|
return messageTemplateService.getDefaultTemplatePlaceholders(type).keys.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun read(key: String, templateType: TemplateType): String {
|
fun read(key: String, templateType: TemplateType): String {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class SetupService(
|
|||||||
password = registration.password,
|
password = registration.password,
|
||||||
email = registration.email,
|
email = registration.email,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
roles = setOf(Role.SUPERADMIN)
|
roles = listOf(Role.SUPERADMIN)
|
||||||
)
|
)
|
||||||
|
|
||||||
val user = userService.registerOrUpdateUser(superAdmin)
|
val user = userService.registerOrUpdateUser(superAdmin)
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ class RoleService(
|
|||||||
return Role.entries.filter { it.powerLevel < highestUserRole.powerLevel }
|
return Role.entries.filter { it.powerLevel < highestUserRole.powerLevel }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authoritiesToRoles(authorities: Collection<GrantedAuthority>): Set<Role> {
|
fun authoritiesToRoles(authorities: Collection<GrantedAuthority>): List<Role> {
|
||||||
return authorities.mapNotNull { Role.safeValueOf(it.authority) }.toMutableSet()
|
return authorities.mapNotNull { Role.safeValueOf(it.authority) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ class UserService(
|
|||||||
val userInfoDto = toUserInfo(oidcUser)
|
val userInfoDto = toUserInfo(oidcUser)
|
||||||
userInfoDto.roles = roleService.extractGrantedAuthorities(principal.authorities)
|
userInfoDto.roles = roleService.extractGrantedAuthorities(principal.authorities)
|
||||||
.mapNotNull { Role.safeValueOf(it.authority) }
|
.mapNotNull { Role.safeValueOf(it.authority) }
|
||||||
.toSet()
|
|
||||||
return userInfoDto
|
return userInfoDto
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +147,7 @@ class UserService(
|
|||||||
password = passwordEncoder.encode(registration.password),
|
password = passwordEncoder.encode(registration.password),
|
||||||
email = registration.email,
|
email = registration.email,
|
||||||
enabled = !adminNeedsToApprove,
|
enabled = !adminNeedsToApprove,
|
||||||
roles = setOf(Role.USER)
|
roles = listOf(Role.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
user = userRepository.save(user)
|
user = userRepository.save(user)
|
||||||
@@ -172,7 +171,7 @@ class UserService(
|
|||||||
email = email,
|
email = email,
|
||||||
emailConfirmed = true,
|
emailConfirmed = true,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
roles = setOf(Role.USER)
|
roles = listOf(Role.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (existsByUsername(user.username)) {
|
if (existsByUsername(user.username)) {
|
||||||
@@ -229,7 +228,7 @@ class UserService(
|
|||||||
return RoleAssignmentResult.ASSIGNED_ROLE_POWER_LEVEL_TOO_HIGH
|
return RoleAssignmentResult.ASSIGNED_ROLE_POWER_LEVEL_TOO_HIGH
|
||||||
}
|
}
|
||||||
|
|
||||||
targetUser.roles = newAssignedRoles.toMutableSet()
|
targetUser.roles = newAssignedRoles
|
||||||
userRepository.save(targetUser)
|
userRepository.save(targetUser)
|
||||||
return RoleAssignmentResult.SUCCESS
|
return RoleAssignmentResult.SUCCESS
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ data class UserInfoDto(
|
|||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
val hasAvatar: Boolean,
|
val hasAvatar: Boolean,
|
||||||
val avatarId: Long? = null,
|
val avatarId: Long? = null,
|
||||||
var roles: Set<Role>
|
var roles: List<Role>
|
||||||
)
|
)
|
||||||
@@ -35,7 +35,7 @@ class User(
|
|||||||
|
|
||||||
@ElementCollection(targetClass = Role::class, fetch = FetchType.EAGER)
|
@ElementCollection(targetClass = Role::class, fetch = FetchType.EAGER)
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
var roles: Set<Role> = emptySet()
|
var roles: List<Role> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(oidcUser: OidcUser) : this(
|
constructor(oidcUser: OidcUser) : this(
|
||||||
|
|||||||
Reference in New Issue
Block a user