mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implement type-safe config for plugins in BE and FE
This commit is contained in:
@@ -35,7 +35,7 @@ dependencies {
|
||||
implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||
|
||||
// Kotlin extensions
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation(kotlin("reflect"))
|
||||
|
||||
// Reactive
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect} from "react";
|
||||
import React from "react";
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import Section from "Frontend/components/general/Section";
|
||||
@@ -11,16 +11,12 @@ import LibraryCreationModal from "Frontend/components/general/modals/LibraryCrea
|
||||
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
||||
import {libraryState} from "Frontend/state/LibraryState";
|
||||
|
||||
function LibraryManagementLayout({getConfig, formik}: any) {
|
||||
const libraryCreationModal = useDisclosure();
|
||||
const state = useSnapshot(libraryState);
|
||||
|
||||
useEffect(() => {
|
||||
initializeLibraryState();
|
||||
}, []);
|
||||
|
||||
async function updateLibrary(library: LibraryUpdateDto) {
|
||||
await LibraryEndpoint.updateLibrary(library);
|
||||
addToast({
|
||||
|
||||
@@ -52,9 +52,11 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
||||
<div className="flex flex-row">
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.auto-register-new-users")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled ||
|
||||
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
||||
<div className="flex flex-row flex-1 justify-center gap-2">
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled ||
|
||||
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="SSO provider configuration"/>
|
||||
|
||||
@@ -23,9 +23,9 @@ import PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins
|
||||
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||
import PluginConfigValidationResult
|
||||
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult";
|
||||
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResult";
|
||||
import PluginConfigValidationResultType
|
||||
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResultType";
|
||||
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResultType";
|
||||
|
||||
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
||||
const pluginDetailsModal = useDisclosure();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import SelectInput from "Frontend/components/general/input/SelectInput";
|
||||
import CheckboxInput from "Frontend/components/general/input/CheckboxInput";
|
||||
import Input from "Frontend/components/general/input/Input";
|
||||
import React from "react";
|
||||
import PluginConfigMetadataDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto";
|
||||
|
||||
export default function PluginConfigFormField({pluginConfigMetadata, ...props}: any) {
|
||||
function inputElement(metadata: PluginConfigMetadataDto) {
|
||||
|
||||
if (metadata.allowedValues != null && metadata.allowedValues.length > 0) {
|
||||
return (
|
||||
<SelectInput label={metadata.label}
|
||||
name={metadata.key}
|
||||
values={metadata.allowedValues}
|
||||
{...props}/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (metadata.type) {
|
||||
case "Boolean":
|
||||
return (
|
||||
<CheckboxInput label={metadata.label}
|
||||
name={metadata.key}
|
||||
{...props}/>
|
||||
);
|
||||
case "String":
|
||||
return (
|
||||
<Input label={metadata.label}
|
||||
name={metadata.key}
|
||||
type={metadata.secret ? "password" : "text"}
|
||||
isRequired={metadata.required}
|
||||
{...props}/>
|
||||
);
|
||||
case "Float":
|
||||
return (
|
||||
<Input label={metadata.label}
|
||||
name={metadata.key}
|
||||
type="number"
|
||||
isRequired={metadata.required}
|
||||
step="0.1"
|
||||
{...props}/>
|
||||
);
|
||||
case "Int":
|
||||
return (
|
||||
<Input label={metadata.label}
|
||||
name={metadata.key}
|
||||
type="number"
|
||||
isRequired={metadata.required}
|
||||
step="1"
|
||||
{...props}/>
|
||||
);
|
||||
default:
|
||||
return <pre>Unsupported type: {metadata.type} for key {metadata.key}</pre>;
|
||||
}
|
||||
}
|
||||
|
||||
return inputElement(pluginConfigMetadata!);
|
||||
}
|
||||
@@ -6,23 +6,19 @@ const SelectInput = ({label, values, ...props}) => {
|
||||
// @ts-ignore
|
||||
const [field] = useField(props);
|
||||
|
||||
const items = values.map((v: string) => ({key: v, label: v}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-1 justify-center gap-2">
|
||||
<Select
|
||||
{...field}
|
||||
{...props}
|
||||
id={field.name}
|
||||
label={label}
|
||||
selectedKeys={[field.value]}
|
||||
disallowEmptySelection
|
||||
>
|
||||
{values.map((value: string) => (
|
||||
<SelectItem key={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
{...field}
|
||||
{...props}
|
||||
label={label}
|
||||
items={items}
|
||||
selectedKeys={[field.value]}
|
||||
disallowEmptySelection
|
||||
>
|
||||
{(item: { key: string, label: string }) => <SelectItem>{item.label}</SelectItem>}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, {useState} from "react";
|
||||
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Tooltip} from "@heroui/react";
|
||||
import {Form, Formik} from "formik";
|
||||
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/plugin/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";
|
||||
import {ArrowClockwise} from "@phosphor-icons/react";
|
||||
import PluginConfigMetadataDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto";
|
||||
import PluginConfigFormField from "Frontend/components/general/input/PluginConfigFormField";
|
||||
|
||||
interface PluginDetailsModalProps {
|
||||
plugin: PluginDto;
|
||||
@@ -35,11 +35,28 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
|
||||
});
|
||||
}
|
||||
|
||||
function getEffectiveConfig(): Record<string, string> {
|
||||
const effectiveConfig: Record<string, string> = {};
|
||||
if (!plugin.configMetadata) return effectiveConfig;
|
||||
|
||||
for (const meta of plugin.configMetadata) {
|
||||
const key = meta.key;
|
||||
let value = plugin.config?.[key]?.toString();
|
||||
if (value == null && meta.default != null) {
|
||||
value = meta.default.toString();
|
||||
}
|
||||
if (value) {
|
||||
effectiveConfig[key] = value;
|
||||
}
|
||||
}
|
||||
return effectiveConfig;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<Formik initialValues={plugin.config}
|
||||
<Formik initialValues={getEffectiveConfig()}
|
||||
initialErrors={plugin.configValidation?.errors}
|
||||
enableReinitialize={true}
|
||||
onSubmit={async (values: any) => {
|
||||
@@ -138,10 +155,11 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
|
||||
</>}
|
||||
</div>
|
||||
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
||||
plugin.configMetadata.map((entry: PluginConfigElement) => (
|
||||
<Input key={entry.key} name={entry.key} label={entry.name}
|
||||
showErrorUntouched={true}
|
||||
type={entry.secret ? "password" : "text"}/>
|
||||
plugin.configMetadata.map((entry: PluginConfigMetadataDto) => (
|
||||
<PluginConfigFormField
|
||||
key={entry.key}
|
||||
pluginConfigMetadata={entry}
|
||||
showErrorUntouched={true}/>
|
||||
)) : "This plugin has no configuration options."
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import {StrictMode} from "react";
|
||||
import {RouterProvider} from "react-router";
|
||||
import router from "./routes";
|
||||
import {router} from './routes';
|
||||
|
||||
const container = document.getElementById('outlet')!;
|
||||
const root = createRoot(container);
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.core.plugins
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.core.Role
|
||||
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.users.util.isAdmin
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@@ -3,16 +3,16 @@ package de.grimsi.gameyfin.core.plugins
|
||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigEntry
|
||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigEntryKey
|
||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
|
||||
import de.grimsi.gameyfin.core.plugins.dto.PluginConfigMetadataDto
|
||||
import de.grimsi.gameyfin.core.plugins.dto.PluginDto
|
||||
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
||||
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginDescriptor
|
||||
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
|
||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementRepository
|
||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.Configurable
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.wrapper.GameyfinPlugin
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.pf4j.ExtensionPoint
|
||||
import org.pf4j.PluginWrapper
|
||||
@@ -96,11 +96,24 @@ class PluginService(
|
||||
return plugin.getLogo()
|
||||
}
|
||||
|
||||
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigElement> {
|
||||
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigMetadataDto>? {
|
||||
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
||||
val plugin = pluginWrapper.plugin
|
||||
if (plugin !is Configurable) return emptyList()
|
||||
return plugin.configMetadata
|
||||
|
||||
if (plugin !is Configurable) return null
|
||||
|
||||
return plugin.configMetadata.map { meta ->
|
||||
PluginConfigMetadataDto(
|
||||
key = meta.key,
|
||||
type = meta.type.simpleName ?: "Unknown",
|
||||
label = meta.label,
|
||||
description = meta.description,
|
||||
default = meta.default,
|
||||
isSecret = meta.isSecret,
|
||||
isRequired = meta.isRequired,
|
||||
allowedValues = meta.allowedValues?.map { it.toString() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package de.grimsi.gameyfin.core.plugins.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import java.io.Serializable
|
||||
|
||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||
class PluginConfigMetadataDto(
|
||||
val key: String,
|
||||
val type: String,
|
||||
val label: String,
|
||||
val description: String,
|
||||
val default: Serializable?,
|
||||
val isSecret: Boolean,
|
||||
val isRequired: Boolean,
|
||||
val allowedValues: List<String>?
|
||||
)
|
||||
@@ -1,8 +1,7 @@
|
||||
package de.grimsi.gameyfin.core.plugins.dto
|
||||
|
||||
import de.grimsi.gameyfin.core.plugins.management.PluginTrustLevel
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||
import org.pf4j.PluginState
|
||||
|
||||
data class PluginDto(
|
||||
@@ -17,7 +16,7 @@ data class PluginDto(
|
||||
val url: String? = null,
|
||||
val hasLogo: Boolean,
|
||||
val state: PluginState,
|
||||
val configMetadata: List<PluginConfigElement>? = null,
|
||||
val configMetadata: List<PluginConfigMetadataDto>? = null,
|
||||
val config: Map<String, String?>? = null,
|
||||
val configValidation: PluginConfigValidationResult? = null,
|
||||
val priority: Int,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.grimsi.gameyfin.core.plugins.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||
import org.pf4j.PluginState
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package de.grimsi.gameyfin.core.plugins.management
|
||||
|
||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
|
||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResultType
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.Configurable
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResultType
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.pf4j.*
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
@@ -164,7 +164,7 @@ class GameyfinPluginManager(
|
||||
fun restart(pluginId: String) {
|
||||
val plugin = getPlugin(pluginId)?.plugin ?: return
|
||||
stopPlugin(pluginId)
|
||||
if (plugin is Configurable) plugin.config = getConfig(pluginId)
|
||||
if (plugin is Configurable) plugin.loadConfig(getConfig(pluginId))
|
||||
startPlugin(pluginId)
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ class GameyfinPluginManager(
|
||||
val plugin = pluginWrapper.plugin
|
||||
if (plugin is Configurable) {
|
||||
val config = getConfig(pluginWrapper.pluginId)
|
||||
plugin.config = config
|
||||
plugin.loadConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-95
@@ -16,20 +16,19 @@ import settings from './build/vaadin-dev-server-settings.json';
|
||||
import {
|
||||
AssetInfo,
|
||||
ChunkInfo,
|
||||
build,
|
||||
defineConfig,
|
||||
mergeConfig,
|
||||
OutputOptions,
|
||||
PluginOption,
|
||||
InlineConfig,
|
||||
UserConfigFn
|
||||
} from 'vite';
|
||||
import { getManifest, type ManifestTransform } from 'workbox-build';
|
||||
|
||||
import * as rollup from 'rollup';
|
||||
import brotli from 'rollup-plugin-brotli';
|
||||
import checker from 'vite-plugin-checker';
|
||||
import postcssLit from './build/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js';
|
||||
import vaadinI18n from './build/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js';
|
||||
import serviceWorkerPlugin from './build/plugins/vite-plugin-service-worker';
|
||||
|
||||
import { createRequire } from 'module';
|
||||
|
||||
@@ -41,8 +40,6 @@ import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js
|
||||
// Make `require` compatible with ES modules
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const appShellUrl = '.';
|
||||
|
||||
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
||||
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
||||
const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput);
|
||||
@@ -56,6 +53,7 @@ const buildOutputFolder = devBundle ? devBundleFolder : frontendBundleFolder;
|
||||
const statsFolder = path.resolve(__dirname, devBundle ? settings.devBundleStatsOutput : settings.statsOutput);
|
||||
const statsFile = path.resolve(statsFolder, 'stats.json');
|
||||
const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html');
|
||||
const i18nFolder = path.resolve(__dirname, settings.i18nOutput);
|
||||
const nodeModulesFolder = path.resolve(__dirname, 'node_modules');
|
||||
const webComponentTags = '';
|
||||
|
||||
@@ -91,94 +89,6 @@ const target = ['safari15', 'es2022'];
|
||||
console.trace = () => {};
|
||||
console.debug = () => {};
|
||||
|
||||
function injectManifestToSWPlugin(): rollup.Plugin {
|
||||
const rewriteManifestIndexHtmlUrl: ManifestTransform = (manifest) => {
|
||||
const indexEntry = manifest.find((entry) => entry.url === 'index.html');
|
||||
if (indexEntry) {
|
||||
indexEntry.url = appShellUrl;
|
||||
}
|
||||
|
||||
return { manifest, warnings: [] };
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'vaadin:inject-manifest-to-sw',
|
||||
async transform(code, id) {
|
||||
if (/sw\.(ts|js)$/.test(id)) {
|
||||
const { manifestEntries } = await getManifest({
|
||||
globDirectory: buildOutputFolder,
|
||||
globPatterns: ['**/*'],
|
||||
globIgnores: ['**/*.br', 'pwa-icons/**'],
|
||||
manifestTransforms: [rewriteManifestIndexHtmlUrl],
|
||||
maximumFileSizeToCacheInBytes: 100 * 1024 * 1024 // 100mb,
|
||||
});
|
||||
|
||||
return code.replace('self.__WB_MANIFEST', JSON.stringify(manifestEntries));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildSWPlugin(opts: { devMode: boolean }): PluginOption {
|
||||
let buildConfig: InlineConfig;
|
||||
let buildOutput: rollup.RollupOutput;
|
||||
const devMode = opts.devMode;
|
||||
|
||||
return {
|
||||
name: 'vaadin:build-sw',
|
||||
enforce: 'post',
|
||||
async configResolved(viteConfig) {
|
||||
buildConfig = {
|
||||
base: viteConfig.base,
|
||||
root: viteConfig.root,
|
||||
mode: viteConfig.mode,
|
||||
resolve: viteConfig.resolve,
|
||||
define: {
|
||||
...viteConfig.define,
|
||||
'process.env.NODE_ENV': JSON.stringify(viteConfig.mode),
|
||||
},
|
||||
build: {
|
||||
write: !devMode,
|
||||
minify: viteConfig.build.minify,
|
||||
outDir: viteConfig.build.outDir,
|
||||
target,
|
||||
sourcemap: viteConfig.command === 'serve' || viteConfig.build.sourcemap,
|
||||
emptyOutDir: false,
|
||||
modulePreload: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
sw: settings.clientServiceWorkerSource
|
||||
},
|
||||
output: {
|
||||
exports: 'none',
|
||||
entryFileNames: 'sw.js',
|
||||
inlineDynamicImports: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
async buildStart() {
|
||||
if (devMode) {
|
||||
buildOutput = await build(buildConfig) as rollup.RollupOutput;
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id.endsWith('sw.js')) {
|
||||
return buildOutput.output[0].code;
|
||||
}
|
||||
},
|
||||
async closeBundle() {
|
||||
if (!devMode) {
|
||||
await build({
|
||||
...buildConfig,
|
||||
plugins: [injectManifestToSWPlugin(), brotli()]
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function statsExtracterPlugin(): PluginOption {
|
||||
function collectThemeJsonsInFrontend(themeJsonContents: Record<string, string>, themeName: string) {
|
||||
const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json');
|
||||
@@ -276,7 +186,7 @@ function statsExtracterPlugin(): PluginOption {
|
||||
const frontendFiles: Record<string, string> = {};
|
||||
frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
||||
|
||||
const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map', '.'];
|
||||
const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'];
|
||||
|
||||
const isThemeComponentsResource = (id: string) =>
|
||||
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
||||
@@ -753,7 +663,9 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
||||
productionMode && brotli(),
|
||||
devMode && vaadinBundlesPlugin(),
|
||||
devMode && showRecompileReason(),
|
||||
settings.offlineEnabled && buildSWPlugin({ devMode }),
|
||||
settings.offlineEnabled && serviceWorkerPlugin({
|
||||
srcPath: settings.clientServiceWorkerSource,
|
||||
}),
|
||||
!devMode && statsExtracterPlugin(),
|
||||
!productionMode && preserveUsageStats(),
|
||||
themePlugin({ devMode }),
|
||||
@@ -795,6 +707,14 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
||||
].filter(Boolean)
|
||||
}
|
||||
}),
|
||||
productionMode && vaadinI18n({
|
||||
cwd: __dirname,
|
||||
meta: {
|
||||
output: {
|
||||
dir: i18nFolder,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'vaadin:force-remove-html-middleware',
|
||||
configureServer(server) {
|
||||
|
||||
@@ -20,13 +20,4 @@ publishing {
|
||||
dependencies {
|
||||
// PF4J (shared)
|
||||
api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}")
|
||||
|
||||
implementation(kotlin("stdlib"))
|
||||
|
||||
// Test dependencies
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core
|
||||
|
||||
interface Configurable {
|
||||
val configMetadata: List<PluginConfigElement>
|
||||
var config: Map<String, String?>
|
||||
|
||||
fun validateConfig(): PluginConfigValidationResult = validateConfig(config)
|
||||
fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core
|
||||
|
||||
import org.pf4j.PluginWrapper
|
||||
|
||||
abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable {
|
||||
|
||||
companion object {
|
||||
lateinit var plugin: ConfigurableGameyfinPlugin
|
||||
private set
|
||||
}
|
||||
|
||||
init {
|
||||
plugin = this
|
||||
}
|
||||
|
||||
override var config: Map<String, String?> = emptyMap()
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core
|
||||
|
||||
data class PluginConfigElement(
|
||||
val key: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val isSecret: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core.config
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
typealias PluginConfigMetadata = List<ConfigMetadata<*>>
|
||||
|
||||
data class ConfigMetadata<T : Serializable>(
|
||||
val key: String,
|
||||
val type: Class<T>,
|
||||
val label: String,
|
||||
val description: String,
|
||||
val default: T? = null,
|
||||
val isSecret: Boolean = false,
|
||||
val isRequired: Boolean = true,
|
||||
) {
|
||||
var allowedValues: List<T>? = null
|
||||
|
||||
init {
|
||||
allowedValues = type.enumConstants?.toList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core.config
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Configurable {
|
||||
val configMetadata: PluginConfigMetadata
|
||||
|
||||
fun loadConfig(config: Map<String, String?>)
|
||||
|
||||
fun validateConfig(): PluginConfigValidationResult
|
||||
fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult
|
||||
|
||||
fun <T : Serializable> config(key: String): T
|
||||
fun <T : Serializable> optionalConfig(key: String): T?
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core
|
||||
package de.grimsi.gameyfin.pluginapi.core.config
|
||||
|
||||
class PluginConfigError(message: String) : RuntimeException(message)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core
|
||||
package de.grimsi.gameyfin.pluginapi.core.config
|
||||
|
||||
data class PluginConfigValidationResult(
|
||||
val result: PluginConfigValidationResultType,
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core.wrapper
|
||||
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.ConfigMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.Configurable
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigError
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||
import org.pf4j.PluginWrapper
|
||||
import java.io.Serializable
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable {
|
||||
|
||||
private var config: Map<String, String?> = emptyMap()
|
||||
|
||||
override fun loadConfig(config: Map<String, String?>) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
override fun validateConfig(): PluginConfigValidationResult = validateConfig(config)
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||
val errors = mutableMapOf<String, String>()
|
||||
|
||||
for (meta in configMetadata) {
|
||||
val value = resolveValue(meta.key, config)
|
||||
if (meta.isRequired && value == null) {
|
||||
errors[meta.key] = "${meta.label} is required"
|
||||
continue
|
||||
}
|
||||
if (value != null) {
|
||||
try {
|
||||
castConfigValue(meta, value)
|
||||
} catch (e: PluginConfigError) {
|
||||
errors[meta.key] = e.message ?: "Invalid value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
PluginConfigValidationResult.VALID
|
||||
} else {
|
||||
PluginConfigValidationResult.INVALID(errors)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Serializable> optionalConfig(key: String): T? {
|
||||
val meta = resolveMetadata(key)
|
||||
val value = resolveValue(key)
|
||||
if (value == null) return null
|
||||
|
||||
return try {
|
||||
castConfigValue(meta, value) as T
|
||||
} catch (e: Exception) {
|
||||
throw PluginConfigError("Failed to cast value for key '$key' to type ${meta.type.simpleName}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun castConfigValue(meta: ConfigMetadata<*>, value: Any): Any? {
|
||||
val expectedType = meta.type
|
||||
return if (expectedType.isEnum) {
|
||||
try {
|
||||
java.lang.Enum.valueOf(expectedType as Class<out Enum<*>>, value.toString())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
throw PluginConfigError("Invalid value '${value}', must be one of ${meta.allowedValues!!.joinToString(", ")}")
|
||||
}
|
||||
} else {
|
||||
if (!expectedType.isInstance(value)) {
|
||||
throw PluginConfigError("Value for key '${meta.key}' is not of type ${expectedType.simpleName}")
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Serializable> config(key: String): T {
|
||||
val value = optionalConfig<T>(key)
|
||||
if (value == null) {
|
||||
throw PluginConfigError("Required configuration key '$key' is missing or has no value")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun resolveMetadata(key: String): ConfigMetadata<*> {
|
||||
return configMetadata.find { it.key == key }
|
||||
?: throw PluginConfigError("Unknown configuration key: $key")
|
||||
}
|
||||
|
||||
private fun resolveValue(key: String, configOverride: Map<String, Serializable?>? = null): Serializable? {
|
||||
val meta = resolveMetadata(key)
|
||||
val conf = configOverride ?: config
|
||||
return conf[key] ?: meta.default
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
package de.grimsi.gameyfin.pluginapi.core
|
||||
package de.grimsi.gameyfin.pluginapi.core.wrapper
|
||||
|
||||
import org.pf4j.Plugin
|
||||
import org.pf4j.PluginWrapper
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
|
||||
|
||||
companion object {
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package de.grimsi.gameyfin.plugins.directdownload
|
||||
|
||||
import de.grimsi.gameyfin.plugins.directdownload.CompressionMode.*
|
||||
import java.util.zip.Deflater
|
||||
|
||||
enum class CompressionMode {
|
||||
NONE,
|
||||
FAST,
|
||||
BEST;
|
||||
}
|
||||
|
||||
fun CompressionMode.deflaterLevel(): Int {
|
||||
return when (this) {
|
||||
NONE -> Deflater.NO_COMPRESSION
|
||||
FAST -> Deflater.BEST_SPEED
|
||||
BEST -> Deflater.BEST_COMPRESSION
|
||||
}
|
||||
}
|
||||
+19
-44
@@ -1,8 +1,8 @@
|
||||
package de.grimsi.gameyfin.plugins.directdownload
|
||||
|
||||
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.ConfigMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.download.Download
|
||||
import de.grimsi.gameyfin.pluginapi.download.DownloadProvider
|
||||
import de.grimsi.gameyfin.pluginapi.download.FileDownload
|
||||
@@ -14,7 +14,6 @@ import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.io.path.exists
|
||||
@@ -24,31 +23,25 @@ import kotlin.io.path.isDirectory
|
||||
|
||||
class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
||||
|
||||
override val configMetadata: List<PluginConfigElement> = listOf(
|
||||
PluginConfigElement(
|
||||
companion object {
|
||||
lateinit var plugin: DirectDownloadPlugin
|
||||
private set
|
||||
}
|
||||
|
||||
init {
|
||||
plugin = this
|
||||
}
|
||||
|
||||
override val configMetadata: PluginConfigMetadata = listOf(
|
||||
ConfigMetadata(
|
||||
key = "compressionMode",
|
||||
name = "Compression mode (\"none\" = default, \"fast\", \"best\")",
|
||||
type = CompressionMode::class.java,
|
||||
label = "Compression mode",
|
||||
description = "Higher compression modes are more resource intensive, but save bandwidth",
|
||||
default = CompressionMode.NONE
|
||||
)
|
||||
)
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||
val compressionMode = config["compressionMode"]
|
||||
|
||||
if (compressionMode != null) {
|
||||
return try {
|
||||
CompressionMode.valueOf(compressionMode.uppercase())
|
||||
PluginConfigValidationResult.VALID
|
||||
} catch (_: IllegalArgumentException) {
|
||||
PluginConfigValidationResult.INVALID(
|
||||
mapOf("compressionMode" to "Invalid compression mode: $compressionMode (must be \"none\", \"fast\", or \"best\")")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return PluginConfigValidationResult.VALID
|
||||
}
|
||||
|
||||
@Extension
|
||||
class DirectDownloadProvider : DownloadProvider {
|
||||
override fun download(path: Path): Download {
|
||||
@@ -95,9 +88,8 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(
|
||||
try {
|
||||
ZipOutputStream(pipeOut).use { zos ->
|
||||
|
||||
zos.setLevel(CompressionMode.toDeflaterLevel(plugin.config["compressionMode"]?.let {
|
||||
CompressionMode.valueOf(it.uppercase())
|
||||
} ?: CompressionMode.NONE))
|
||||
val compressionMode = plugin.config<CompressionMode>("compressionMode")
|
||||
zos.setLevel(compressionMode.deflaterLevel())
|
||||
|
||||
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
@@ -124,21 +116,4 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(
|
||||
return pipeIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class CompressionMode {
|
||||
NONE,
|
||||
FAST,
|
||||
BEST;
|
||||
|
||||
companion object {
|
||||
fun toDeflaterLevel(mode: CompressionMode): Int {
|
||||
return when (mode) {
|
||||
NONE -> Deflater.NO_COMPRESSION
|
||||
FAST -> Deflater.BEST_SPEED
|
||||
BEST -> Deflater.BEST_COMPRESSION
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import com.api.igdb.exceptions.RequestException
|
||||
import com.api.igdb.request.IGDBWrapper
|
||||
import com.api.igdb.request.TwitchAuthenticator
|
||||
import com.api.igdb.request.games
|
||||
import de.grimsi.gameyfin.pluginapi.core.*
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.*
|
||||
import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
@@ -16,26 +17,35 @@ import proto.Game
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable {
|
||||
class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
||||
|
||||
override val configMetadata = listOf(
|
||||
PluginConfigElement(
|
||||
override val configMetadata: PluginConfigMetadata = listOf(
|
||||
ConfigMetadata(
|
||||
key = "clientId",
|
||||
name = "Twitch client ID",
|
||||
type = String::class.java,
|
||||
label = "Twitch client ID",
|
||||
description = "Your Twitch Client ID"
|
||||
),
|
||||
PluginConfigElement(
|
||||
ConfigMetadata(
|
||||
key = "clientSecret",
|
||||
name = "Twitch client secret",
|
||||
type = String::class.java,
|
||||
label = "Twitch client secret",
|
||||
description = "Your Twitch Client Secret",
|
||||
isSecret = true
|
||||
)
|
||||
)
|
||||
override var config: Map<String, String?> = emptyMap()
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||
val pluginConfigValidationResult = super.validateConfig(config)
|
||||
|
||||
if (pluginConfigValidationResult.result == PluginConfigValidationResultType.INVALID) {
|
||||
return pluginConfigValidationResult
|
||||
}
|
||||
|
||||
try {
|
||||
authenticate(config["clientId"], config["clientSecret"])
|
||||
val clientIdToValidate = config["clientId"]
|
||||
val clientSecretToValidate = config["clientSecret"]
|
||||
authenticate(clientIdToValidate, clientSecretToValidate)
|
||||
return PluginConfigValidationResult.VALID
|
||||
} catch (e: PluginConfigError) {
|
||||
log.error(e.message)
|
||||
@@ -50,7 +60,7 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable
|
||||
|
||||
override fun start() {
|
||||
try {
|
||||
authenticate(config["clientId"], config["clientSecret"])
|
||||
authenticate(config("clientId"), config("clientSecret"))
|
||||
} catch (e: PluginConfigError) {
|
||||
log.error(e.message)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.plugins.steam
|
||||
|
||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.wrapper.GameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||
import de.grimsi.gameyfin.plugins.steam.dto.SteamDetailsResultWrapper
|
||||
|
||||
+16
-10
@@ -1,9 +1,7 @@
|
||||
package de.grimsi.gameyfin.plugins.steamgriddb
|
||||
|
||||
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||
import de.grimsi.gameyfin.pluginapi.core.config.*
|
||||
import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||
import de.grimsi.gameyfin.plugins.steamgriddb.api.SteamGridDbApiClient
|
||||
@@ -20,18 +18,26 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
||||
private var client: SteamGridDbApiClient? = null
|
||||
}
|
||||
|
||||
override val configMetadata: List<PluginConfigElement> = listOf(
|
||||
PluginConfigElement(
|
||||
override val configMetadata: PluginConfigMetadata = listOf(
|
||||
ConfigMetadata(
|
||||
key = "apiKey",
|
||||
name = "SteamGridDB API key",
|
||||
description = "Your SteamGridDB API key",
|
||||
type = String::class.java,
|
||||
label = "SteamGridDB API key",
|
||||
description = "The API key can be obtained from your SteamGridDB account preferences",
|
||||
isSecret = true
|
||||
)
|
||||
)
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||
val pluginConfigValidationResult = super.validateConfig(config)
|
||||
|
||||
if (pluginConfigValidationResult.result == PluginConfigValidationResultType.INVALID) {
|
||||
return pluginConfigValidationResult
|
||||
}
|
||||
|
||||
try {
|
||||
runBlocking { authenticate(config["apiKey"]) }
|
||||
val apiKeyToValidate = config["apiKey"]
|
||||
runBlocking { authenticate(apiKeyToValidate) }
|
||||
return PluginConfigValidationResult.VALID
|
||||
} catch (e: PluginConfigError) {
|
||||
log.error(e.message)
|
||||
@@ -43,7 +49,7 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
||||
|
||||
override fun start() {
|
||||
try {
|
||||
runBlocking { authenticate(config["apiKey"]) }
|
||||
runBlocking { authenticate(config("apiKey")) }
|
||||
} catch (e: PluginConfigError) {
|
||||
log.error(e.message)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user