mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +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",
|
||||
"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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 && (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h2 className="text-2xl font-bold">Plugins</h2>
|
||||
</div>
|
||||
<Divider className="mb-4"/>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
{pluginTypes.map(type =>
|
||||
<PluginManagementSection key={type} pluginType={type}/>
|
||||
// @ts-ignore
|
||||
<PluginManagementSection key={type} type={type} plugins={state.pluginsByType[type]}/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<any>, title: String, validationSchema?: any) {
|
||||
@@ -16,7 +16,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
const state = useSnapshot(configState);
|
||||
|
||||
useEffect(() => {
|
||||
initializeConfig();
|
||||
initializeConfigState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,14 +26,14 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
}, [configSaved])
|
||||
|
||||
async function handleSubmit(values: NestedConfig): Promise<void> {
|
||||
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<string, any> {
|
||||
@@ -86,7 +86,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
<>
|
||||
{state.isLoaded ?
|
||||
<Formik
|
||||
initialValues={state.configNested}
|
||||
initialValues={state.config}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validationSchema}
|
||||
enableReinitialize={true}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Plug} from "@phosphor-icons/react";
|
||||
import React from "react";
|
||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
|
||||
import {Image} from "@heroui/react";
|
||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||
|
||||
interface PluginLogoProps {
|
||||
plugin: PluginDto;
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
import {Button, Tooltip, useDisclosure} from "@heroui/react";
|
||||
import {ListNumbers} from "@phosphor-icons/react";
|
||||
import {PluginManagementCard} from "Frontend/components/general/cards/PluginManagementCard";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
|
||||
import {PluginManagementEndpoint} from "Frontend/generated/endpoints";
|
||||
import React from "react";
|
||||
import PluginPrioritiesModal from "Frontend/components/general/modals/PluginPrioritiesModal";
|
||||
import {camelCaseToTitle} from "Frontend/util/utils";
|
||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||
|
||||
interface PluginManagementSectionProps {
|
||||
pluginType: string;
|
||||
type: string;
|
||||
plugins: PluginDto[];
|
||||
}
|
||||
|
||||
export function PluginManagementSection({pluginType}: PluginManagementSectionProps) {
|
||||
const [plugins, setPlugins] = useState<PluginDto[]>([]);
|
||||
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 (
|
||||
<div className="flex flex-col gap-2">
|
||||
<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">
|
||||
<Button isIconOnly variant="flat" onPress={pluginPrioritiesModal.onOpen}>
|
||||
@@ -45,9 +27,8 @@ export function PluginManagementSection({pluginType}: PluginManagementSectionPro
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-300px gap-4">
|
||||
{plugins.map((plugin) => <PluginManagementCard plugin={plugin}
|
||||
updatePlugin={updatePlugin}
|
||||
key={plugin.name}/>
|
||||
{plugins.map((plugin) =>
|
||||
<PluginManagementCard plugin={plugin} key={plugin.id}/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<PluginConfigValidationResult | undefined>(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 <PlayCircle/>;
|
||||
case PluginState.DISABLED:
|
||||
return <PauseCircle/>;
|
||||
case PluginState.STOPPED:
|
||||
case PluginState.FAILED:
|
||||
return <StopCircle/>;
|
||||
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}: {
|
||||
<PluginDetailsModal plugin={plugin}
|
||||
isOpen={pluginDetailsModal.isOpen}
|
||||
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 {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<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>);
|
||||
});
|
||||
}, []);
|
||||
|
||||
export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) {
|
||||
async function saveConfig(values: Record<string, string>) {
|
||||
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 (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<Formik initialValues={pluginConfig}
|
||||
<Formik initialValues={plugin.config}
|
||||
enableReinitialize={true}
|
||||
onSubmit={async (values: any) => {
|
||||
await saveConfig(values);
|
||||
@@ -107,8 +89,8 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
|
||||
</div>
|
||||
|
||||
<h4 className="text-l font-bold mt-4">Configuration</h4>
|
||||
{(pluginConfigMeta && pluginConfigMeta.length > 0) ?
|
||||
pluginConfigMeta.map((entry: PluginConfigElement) => (
|
||||
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
||||
plugin.configMetadata.map((entry: PluginConfigElement) => (
|
||||
<Input key={entry.key} name={entry.key} label={entry.name}
|
||||
type={entry.secret ? "password" : "text"}/>
|
||||
)) : "This plugin has no configuration options."
|
||||
@@ -118,7 +100,7 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{(pluginConfigMeta && pluginConfigMeta?.length > 0) ?
|
||||
{(plugin.configMetadata && plugin.configMetadata?.length > 0) ?
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "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 {CaretUpDown} from "@phosphor-icons/react";
|
||||
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 {
|
||||
plugins: PluginDto[];
|
||||
@@ -51,11 +51,11 @@ export default function PluginPrioritiesModal({plugins, isOpen, onOpenChange}: P
|
||||
async function setPluginPriorities(onClose: () => void) {
|
||||
try {
|
||||
const prioritiesMap = generatePrioritiesMap();
|
||||
await PluginManagementEndpoint.setPluginPriorities(prioritiesMap);
|
||||
await PluginEndpoint.setPluginPriorities(prioritiesMap);
|
||||
|
||||
addToast({
|
||||
title: "Plugin order updated",
|
||||
description: "Plugin order have been updated successfully.",
|
||||
description: "Plugin order has been updated successfully.",
|
||||
color: "success"
|
||||
});
|
||||
onClose();
|
||||
|
||||
@@ -7,35 +7,35 @@ import {Subscription} from "@vaadin/hilla-frontend";
|
||||
type ConfigState = {
|
||||
subscription?: Subscription<ConfigUpdateDto>;
|
||||
isLoaded: boolean;
|
||||
configEntries: Record<string, ConfigEntryDto>;
|
||||
configNested: NestedConfig;
|
||||
state: Record<string, ConfigEntryDto>;
|
||||
config: NestedConfig;
|
||||
};
|
||||
|
||||
export const configState = proxy<ConfigState>({
|
||||
get isLoaded() {
|
||||
return this.subscription != null;
|
||||
},
|
||||
configEntries: {},
|
||||
get configNested() {
|
||||
return toNestedConfig(Object.values(this.configEntries));
|
||||
state: {},
|
||||
get config() {
|
||||
return toNestedConfig(Object.values(this.state));
|
||||
}
|
||||
});
|
||||
|
||||
/** Subscribe to and process state updates from backend **/
|
||||
export async function initializeConfig() {
|
||||
export async function initializeConfigState() {
|
||||
if (configState.isLoaded) return;
|
||||
|
||||
// Fetch initial configuration data
|
||||
const initialEntries = await ConfigEndpoint.getAll();
|
||||
initialEntries.forEach((entry) => {
|
||||
configState.configEntries[entry.key] = entry;
|
||||
configState.state[entry.key] = entry;
|
||||
});
|
||||
|
||||
// Subscribe to real-time updates
|
||||
configState.subscription = ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
|
||||
Object.entries(updateDto.updates).forEach(([key, value]) => {
|
||||
if (configState.configEntries[key]) {
|
||||
configState.configEntries[key].value = value;
|
||||
if (configState.state[key]) {
|
||||
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.userdetails.UserDetails
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
|
||||
@Endpoint
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
class ConfigEndpoint(
|
||||
private val config: ConfigService
|
||||
private val configService: ConfigService
|
||||
) {
|
||||
|
||||
/** CRUD endpoints for admins **/
|
||||
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
|
||||
|
||||
fun getAll(): List<ConfigEntryDto> {
|
||||
return config.getAll(null)
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
fun subscribe(): Flux<ConfigUpdateDto> {
|
||||
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
|
||||
return if (user.isAdmin()) configUpdates.asFlux()
|
||||
return if (user.isAdmin()) configService.subscribe()
|
||||
else Flux.empty()
|
||||
}
|
||||
|
||||
fun update(update: ConfigUpdateDto) {
|
||||
config.update(update.updates)
|
||||
configUpdates.tryEmitNext(update)
|
||||
}
|
||||
fun getAll(): List<ConfigEntryDto> = configService.getAll()
|
||||
|
||||
fun update(update: ConfigUpdateDto) = configService.update(update)
|
||||
|
||||
/** Specific read-only endpoint for all users **/
|
||||
|
||||
@PermitAll
|
||||
fun isSsoEnabled(): Boolean? {
|
||||
return config.get(ConfigProperties.SSO.OIDC.Enabled)
|
||||
}
|
||||
fun isSsoEnabled(): Boolean? = configService.get(ConfigProperties.SSO.OIDC.Enabled)
|
||||
|
||||
@PermitAll
|
||||
fun getLogoutUrl(): String? {
|
||||
return config.get(ConfigProperties.SSO.OIDC.LogoutUrl)
|
||||
}
|
||||
fun getLogoutUrl(): String? = configService.get(ConfigProperties.SSO.OIDC.LogoutUrl)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
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.persistence.ConfigRepository
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
import java.io.Serializable
|
||||
|
||||
@Service
|
||||
@@ -14,6 +17,11 @@ class ConfigService(
|
||||
) {
|
||||
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.
|
||||
@@ -52,21 +60,16 @@ class ConfigService(
|
||||
/**
|
||||
* Get all known config values.
|
||||
*
|
||||
* @param prefix: Optional prefix to filter the 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()
|
||||
}
|
||||
|
||||
if (prefix != null) {
|
||||
configProperties = configProperties.filter { it.key.startsWith(prefix) }
|
||||
}
|
||||
|
||||
return configProperties.map { configProperty ->
|
||||
ConfigEntryDto(
|
||||
key = configProperty.key,
|
||||
@@ -115,16 +118,17 @@ class ConfigService(
|
||||
* Set multiple config values at once.
|
||||
* 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?>) {
|
||||
updates.forEach { (key, value) ->
|
||||
fun update(update: ConfigUpdateDto) {
|
||||
update.updates.forEach { (key, value) ->
|
||||
if (value == null) {
|
||||
delete(key)
|
||||
} else {
|
||||
set(key, value)
|
||||
}
|
||||
}
|
||||
configUpdates.tryEmitNext(update)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ class SetupDataLoader(
|
||||
email = "admin@gameyfin.org",
|
||||
emailConfirmed = true,
|
||||
enabled = true,
|
||||
roles = setOf(Role.SUPERADMIN)
|
||||
roles = listOf(Role.SUPERADMIN)
|
||||
)
|
||||
|
||||
registerUserIfNotFound(superadmin)
|
||||
@@ -56,7 +56,7 @@ class SetupDataLoader(
|
||||
email = "user@gameyfin.org",
|
||||
emailConfirmed = true,
|
||||
enabled = true,
|
||||
roles = setOf(Role.USER)
|
||||
roles = listOf(Role.USER)
|
||||
)
|
||||
|
||||
registerUserIfNotFound(user)
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.core.filesystem
|
||||
import java.nio.file.Path
|
||||
|
||||
data class FilesystemScanResult(
|
||||
val newPaths: Set<Path>,
|
||||
val removedGamePaths: Set<Path>,
|
||||
val removedUnmatchedPaths: Set<Path>
|
||||
val newPaths: List<Path>,
|
||||
val removedGamePaths: List<Path>,
|
||||
val removedUnmatchedPaths: List<Path>
|
||||
)
|
||||
|
||||
@@ -116,18 +116,18 @@ class FilesystemService(
|
||||
val newPaths = currentFilesystemPaths.filter { path ->
|
||||
val isInLibrary = allCurrentLibraryPaths.any { it == path }
|
||||
!isInLibrary
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
//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 isOnFilesystem = currentFilesystemPaths.any { it == path }
|
||||
!isOnFilesystem
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
val removedUnmatchedPaths = currentLibraryUnmatchedPaths.filter { path ->
|
||||
val isOnFilesystem = currentFilesystemPaths.any { it == path }
|
||||
!isOnFilesystem
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
return FilesystemScanResult(
|
||||
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.PluginConfigElement
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.pf4j.PluginWrapper
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
@@ -14,36 +15,22 @@ class PluginConfigService(
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
fun getConfigMetadata(pluginId: String): List<PluginConfigElement> {
|
||||
log.debug { "Getting config metadata for plugin $pluginId" }
|
||||
|
||||
val plugin = try {
|
||||
pluginManager.getPlugin(pluginId).plugin
|
||||
} catch (_: NoClassDefFoundError) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigElement> {
|
||||
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
||||
val plugin = pluginWrapper.plugin
|
||||
if (plugin !is Configurable) return emptyList()
|
||||
|
||||
return plugin.configMetadata
|
||||
}
|
||||
|
||||
fun getConfig(pluginId: String): Map<String, String?> {
|
||||
log.debug { "Getting config for plugin $pluginId" }
|
||||
return pluginConfigRepository.findAllById_PluginId(pluginId).associate { it.id.key to it.value }
|
||||
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||
log.debug { "Getting config for plugin ${pluginWrapper.pluginId}" }
|
||||
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" }
|
||||
val entries = config.map { PluginConfigEntry(PluginConfigEntryKey(pluginId, it.key), it.value) }
|
||||
pluginConfigRepository.saveAll(entries)
|
||||
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
|
||||
|
||||
data class PluginDto(
|
||||
val id: String,
|
||||
val types: List<String>,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val shortDescription: String? = null,
|
||||
@@ -13,6 +16,8 @@ data class PluginDto(
|
||||
val url: String? = null,
|
||||
val hasLogo: Boolean,
|
||||
val state: PluginState,
|
||||
val configMetadata: List<PluginConfigElement>? = null,
|
||||
val config: Map<String, String?>? = null,
|
||||
val priority: Int,
|
||||
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
|
||||
}
|
||||
|
||||
fun getExtensionTypeClasses(pluginId: String): Set<Class<ExtensionPoint>> {
|
||||
fun getExtensionTypeClasses(pluginId: String): List<Class<ExtensionPoint>> {
|
||||
return getExtensionClasses(pluginId)
|
||||
.flatMap { it.interfaces.toList() }
|
||||
.filterIsInstance<Class<ExtensionPoint>>()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
fun getExtensionTypes(pluginId: String): Set<String> {
|
||||
fun getExtensionTypes(pluginId: String): List<String> {
|
||||
return getExtensionClasses(pluginId)
|
||||
.flatMap { it.interfaces.toList() }
|
||||
.filterIsInstance<Class<ExtensionPoint>>()
|
||||
.map { it.simpleName }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigService
|
||||
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 org.pf4j.ExtensionPoint
|
||||
import org.pf4j.PluginWrapper
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
|
||||
@Service
|
||||
class PluginManagementService(
|
||||
private val pluginManager: GameyfinPluginManager,
|
||||
private val pluginConfigService: PluginConfigService,
|
||||
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
|
||||
.flatMap { pluginManager.getExtensionTypes(it.pluginId) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
fun getPluginDtos(type: String?): Set<PluginDto> {
|
||||
fun getAll(): List<PluginDto> {
|
||||
return pluginManager.plugins
|
||||
.filter { type == null || type in pluginManager.getExtensionTypes(it.pluginId) }
|
||||
.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 {
|
||||
@@ -51,18 +51,6 @@ class PluginManagementService(
|
||||
?: 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) {
|
||||
pluginManager.enablePlugin(pluginId)
|
||||
}
|
||||
@@ -75,10 +63,10 @@ class PluginManagementService(
|
||||
return pluginManager.validatePluginConfig(pluginId)
|
||||
}
|
||||
|
||||
fun setPluginPriority(pluginId: String, priority: Int) {
|
||||
val pluginManagementEntry = getPluginManagementEntry(pluginId)
|
||||
pluginManagementEntry.priority = priority
|
||||
pluginManagementRepository.save(pluginManagementEntry)
|
||||
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) {
|
||||
pluginConfigService.updateConfig(pluginId, updatedConfig)
|
||||
val update = PluginUpdateDto(pluginId, config = updatedConfig)
|
||||
pluginUpdates.tryEmitNext(update)
|
||||
}
|
||||
|
||||
fun setPluginPriorities(pluginPriorities: Map<String, Int>) {
|
||||
@@ -110,6 +98,7 @@ class PluginManagementService(
|
||||
|
||||
return PluginDto(
|
||||
id = descriptor.pluginId,
|
||||
types = pluginManager.getExtensionTypes(pluginWrapper.pluginId),
|
||||
name = descriptor.pluginName,
|
||||
description = descriptor.pluginDescription,
|
||||
shortDescription = descriptor.pluginShortDescription,
|
||||
@@ -119,6 +108,8 @@ class PluginManagementService(
|
||||
url = descriptor.pluginUrl,
|
||||
hasLogo = hasLogo,
|
||||
state = pluginWrapper.pluginState,
|
||||
configMetadata = pluginConfigService.getConfigMetadata(pluginWrapper),
|
||||
config = pluginConfigService.getConfig(pluginWrapper),
|
||||
priority = pluginManagementEntry.priority,
|
||||
trustLevel = pluginManagementEntry.trustLevel
|
||||
)
|
||||
|
||||
@@ -47,12 +47,12 @@ class GameService(
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun create(games: Collection<Game>): Collection<Game> {
|
||||
fun create(games: List<Game>): List<Game> {
|
||||
val gamesToBePersisted = games.filter { it.id == null }
|
||||
|
||||
gamesToBePersisted.forEach { game ->
|
||||
game.publishers = game.publishers.map { companyService.createOrGet(it) }.toSet()
|
||||
game.developers = game.developers.map { companyService.createOrGet(it) }.toSet()
|
||||
game.publishers = game.publishers.map { companyService.createOrGet(it) }
|
||||
game.developers = game.developers.map { companyService.createOrGet(it) }
|
||||
game
|
||||
}
|
||||
|
||||
@@ -87,11 +87,11 @@ class GameService(
|
||||
return mergedGame
|
||||
}
|
||||
|
||||
fun getAllByPaths(paths: Collection<String>): Collection<Game> {
|
||||
fun getAllByPaths(paths: List<String>): List<Game> {
|
||||
return gameRepository.findAllByPathIn(paths)
|
||||
}
|
||||
|
||||
fun getAllGames(): Collection<GameDto> {
|
||||
fun getAllGames(): List<GameDto> {
|
||||
val entities = gameRepository.findAll()
|
||||
return entities.map { it.toDto() }
|
||||
}
|
||||
@@ -231,58 +231,58 @@ class GameService(
|
||||
metadata.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy ->
|
||||
if (!metadataMap.containsKey("publishers")) {
|
||||
mergedGame.publishers =
|
||||
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }.toSet()
|
||||
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }
|
||||
metadataMap["publishers"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy ->
|
||||
if (!metadataMap.containsKey("developers")) {
|
||||
mergedGame.developers =
|
||||
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }.toSet()
|
||||
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }
|
||||
metadataMap["developers"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.genres?.takeIf { it.isNotEmpty() }?.let { genres ->
|
||||
if (!metadataMap.containsKey("genres")) {
|
||||
mergedGame.genres = genres
|
||||
mergedGame.genres = genres.toList()
|
||||
metadataMap["genres"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.themes?.takeIf { it.isNotEmpty() }?.let { themes ->
|
||||
if (!metadataMap.containsKey("themes")) {
|
||||
mergedGame.themes = themes
|
||||
mergedGame.themes = themes.toList()
|
||||
metadataMap["themes"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.keywords?.takeIf { it.isNotEmpty() }?.let { keywords ->
|
||||
if (!metadataMap.containsKey("keywords")) {
|
||||
mergedGame.keywords = keywords
|
||||
mergedGame.keywords = keywords.toList()
|
||||
metadataMap["keywords"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.features?.takeIf { it.isNotEmpty() }?.let { features ->
|
||||
if (!metadataMap.containsKey("features")) {
|
||||
mergedGame.features = features
|
||||
mergedGame.features = features.toList()
|
||||
metadataMap["features"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.perspectives?.takeIf { it.isNotEmpty() }?.let { perspectives ->
|
||||
if (!metadataMap.containsKey("perspectives")) {
|
||||
mergedGame.perspectives = perspectives
|
||||
mergedGame.perspectives = perspectives.toList()
|
||||
metadataMap["perspectives"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
|
||||
if (!metadataMap.containsKey("images")) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
metadata.videoUrls?.takeIf { it.isNotEmpty() }?.let { videoUrls ->
|
||||
if (!metadataMap.containsKey("videoUrls")) {
|
||||
mergedGame.videoUrls = videoUrls
|
||||
mergedGame.videoUrls = videoUrls.toList()
|
||||
metadataMap["videoUrls"] = FieldMetadata(sourcePlugin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,31 +49,31 @@ class Game(
|
||||
var criticRating: Int? = null,
|
||||
|
||||
@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])
|
||||
var developers: Set<Company> = emptySet(),
|
||||
var developers: List<Company> = emptyList(),
|
||||
|
||||
@ElementCollection(targetClass = Genre::class)
|
||||
var genres: Set<Genre> = emptySet(),
|
||||
var genres: List<Genre> = emptyList(),
|
||||
|
||||
@ElementCollection(targetClass = Theme::class)
|
||||
var themes: Set<Theme> = emptySet(),
|
||||
var themes: List<Theme> = emptyList(),
|
||||
|
||||
@ElementCollection
|
||||
var keywords: Set<String> = emptySet(),
|
||||
var keywords: List<String> = emptyList(),
|
||||
|
||||
@ElementCollection(targetClass = GameFeature::class)
|
||||
var features: Set<GameFeature> = emptySet(),
|
||||
var features: List<GameFeature> = emptyList(),
|
||||
|
||||
@ElementCollection(targetClass = PlayerPerspective::class)
|
||||
var perspectives: Set<PlayerPerspective>? = null,
|
||||
var perspectives: List<PlayerPerspective>? = null,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||
var images: Set<Image> = emptySet(),
|
||||
var images: List<Image> = emptyList(),
|
||||
|
||||
@ElementCollection
|
||||
var videoUrls: Set<URI> = emptySet(),
|
||||
var videoUrls: List<URI> = emptyList(),
|
||||
|
||||
@Column(unique = true)
|
||||
val path: String,
|
||||
|
||||
@@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface GameRepository : JpaRepository<Game, Long> {
|
||||
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 findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
|
||||
}
|
||||
@@ -12,11 +12,11 @@ class Library(
|
||||
var name: String,
|
||||
|
||||
@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)
|
||||
var games: MutableSet<Game> = HashSet(),
|
||||
var games: MutableList<Game> = ArrayList(),
|
||||
|
||||
@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
|
||||
|
||||
data class LibraryScanResult(
|
||||
val libraries: Set<Library>,
|
||||
val newGames: Set<Game>,
|
||||
val removedGames: Set<Game>,
|
||||
val newUnmatchedPaths: Set<String>
|
||||
val libraries: List<Library>,
|
||||
val newGames: List<Game>,
|
||||
val removedGames: List<Game>,
|
||||
val newUnmatchedPaths: List<String>
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ class LibraryService(
|
||||
libraryDto.directories?.let {
|
||||
existingLibrary.directories = it
|
||||
.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
||||
.toMutableSet()
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
val updatedLibrary = libraryRepository.save(existingLibrary)
|
||||
@@ -271,10 +271,10 @@ class LibraryService(
|
||||
libraryRepository.save(library)
|
||||
|
||||
return LibraryScanResult(
|
||||
libraries = setOf(library),
|
||||
newGames = persistedGames.toSet(),
|
||||
removedGames = removedGames.toSet(),
|
||||
newUnmatchedPaths = newUnmatchedPaths
|
||||
libraries = listOf(library),
|
||||
newGames = persistedGames,
|
||||
removedGames = removedGames,
|
||||
newUnmatchedPaths = newUnmatchedPaths.toList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ class LibraryService(
|
||||
name = library.name,
|
||||
directories = library.directories.map {
|
||||
DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath)
|
||||
}.toMutableSet()
|
||||
}.toMutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,5 @@ package de.grimsi.gameyfin.libraries.dto
|
||||
data class LibraryUpdateDto(
|
||||
val id: Long,
|
||||
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)
|
||||
}
|
||||
|
||||
fun getDefaultPlaceholders(type: TemplateType): Set<String> {
|
||||
return messageTemplateService.getDefaultTemplatePlaceholders(type).keys
|
||||
fun getDefaultPlaceholders(type: TemplateType): List<String> {
|
||||
return messageTemplateService.getDefaultTemplatePlaceholders(type).keys.toList()
|
||||
}
|
||||
|
||||
fun read(key: String, templateType: TemplateType): String {
|
||||
|
||||
@@ -32,7 +32,7 @@ class SetupService(
|
||||
password = registration.password,
|
||||
email = registration.email,
|
||||
enabled = true,
|
||||
roles = setOf(Role.SUPERADMIN)
|
||||
roles = listOf(Role.SUPERADMIN)
|
||||
)
|
||||
|
||||
val user = userService.registerOrUpdateUser(superAdmin)
|
||||
|
||||
@@ -48,8 +48,8 @@ class RoleService(
|
||||
return Role.entries.filter { it.powerLevel < highestUserRole.powerLevel }
|
||||
}
|
||||
|
||||
fun authoritiesToRoles(authorities: Collection<GrantedAuthority>): Set<Role> {
|
||||
return authorities.mapNotNull { Role.safeValueOf(it.authority) }.toMutableSet()
|
||||
fun authoritiesToRoles(authorities: Collection<GrantedAuthority>): List<Role> {
|
||||
return authorities.mapNotNull { Role.safeValueOf(it.authority) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -88,7 +88,6 @@ class UserService(
|
||||
val userInfoDto = toUserInfo(oidcUser)
|
||||
userInfoDto.roles = roleService.extractGrantedAuthorities(principal.authorities)
|
||||
.mapNotNull { Role.safeValueOf(it.authority) }
|
||||
.toSet()
|
||||
return userInfoDto
|
||||
}
|
||||
|
||||
@@ -148,7 +147,7 @@ class UserService(
|
||||
password = passwordEncoder.encode(registration.password),
|
||||
email = registration.email,
|
||||
enabled = !adminNeedsToApprove,
|
||||
roles = setOf(Role.USER)
|
||||
roles = listOf(Role.USER)
|
||||
)
|
||||
|
||||
user = userRepository.save(user)
|
||||
@@ -172,7 +171,7 @@ class UserService(
|
||||
email = email,
|
||||
emailConfirmed = true,
|
||||
enabled = true,
|
||||
roles = setOf(Role.USER)
|
||||
roles = listOf(Role.USER)
|
||||
)
|
||||
|
||||
if (existsByUsername(user.username)) {
|
||||
@@ -229,7 +228,7 @@ class UserService(
|
||||
return RoleAssignmentResult.ASSIGNED_ROLE_POWER_LEVEL_TOO_HIGH
|
||||
}
|
||||
|
||||
targetUser.roles = newAssignedRoles.toMutableSet()
|
||||
targetUser.roles = newAssignedRoles
|
||||
userRepository.save(targetUser)
|
||||
return RoleAssignmentResult.SUCCESS
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ data class UserInfoDto(
|
||||
val isEnabled: Boolean,
|
||||
val hasAvatar: Boolean,
|
||||
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)
|
||||
@Enumerated(EnumType.STRING)
|
||||
var roles: Set<Role> = emptySet()
|
||||
var roles: List<Role> = emptyList()
|
||||
) {
|
||||
|
||||
constructor(oidcUser: OidcUser) : this(
|
||||
|
||||
Reference in New Issue
Block a user