diff --git a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx index ce78a5c..1e00686 100644 --- a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx @@ -19,11 +19,19 @@ export default function PluginManagement() {

Plugins

- + +
+

Metadata

+
+
{plugins.map((plugin) => )}
+ +
+

Notifications

+
); } \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/PluginConfigurationModal.tsx b/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx similarity index 55% rename from gameyfin/src/main/frontend/components/general/PluginConfigurationModal.tsx rename to gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx index ffc8855..9240304 100644 --- a/gameyfin/src/main/frontend/components/general/PluginConfigurationModal.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx @@ -6,14 +6,15 @@ import {PluginConfigEndpoint} 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"; +import {PuzzlePiece} from "@phosphor-icons/react"; -interface PluginConfigurationModalProps { +interface PluginDetailsModalProps { plugin: PluginDto; isOpen: boolean; onOpenChange: () => void; } -export default function PluginConfigurationModal({plugin, isOpen, onOpenChange}: PluginConfigurationModalProps) { +export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) { const [pluginConfigMeta, setPluginConfigMeta] = useState<(PluginConfigElement)[]>(); const [pluginConfig, setPluginConfig] = useState>(); @@ -46,24 +47,40 @@ export default function PluginConfigurationModal({plugin, isOpen, onOpenChange}: > {(formik: { isSubmitting: any; }) => (
- {plugin.name} configuration + + Plugin configuration for {plugin.name} - {pluginConfigMeta && pluginConfigMeta.map((entry: any) => ( - - ))} +

Details

+
+ +
+

Author: {plugin.author}

+

Version: {plugin.version}

+

Plugin ID: {plugin.id}

+

Status: {plugin.state?.toLowerCase()}

+
+
+ +

Configuration

+ {(pluginConfigMeta && pluginConfigMeta.length > 0) ? + pluginConfigMeta.map((entry: any) => ( + + )) : "This plugin has no configuration options." + }
- + {(pluginConfigMeta && pluginConfigMeta?.length > 0) ? + : ""}
)} diff --git a/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx b/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx index 2ad5825..06e52cf 100644 --- a/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx @@ -1,14 +1,29 @@ -import {Card, Chip, Tooltip, useDisclosure} from "@nextui-org/react"; +import {Card, Chip, Skeleton, useDisclosure} from "@nextui-org/react"; import {PuzzlePiece} 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 from "react"; -import PluginConfigurationModal from "Frontend/components/general/PluginConfigurationModal"; +import React, {useEffect, useState} from "react"; +import PluginDetailsModal from "Frontend/components/general/PluginDetailsModal"; export function PluginManagementCard({plugin}: { plugin: PluginDto }) { - const pluginConfigurationModal = useDisclosure(); + const pluginDetailsModal = useDisclosure(); + const [configValid, setConfigValid] = useState(undefined); - function stateToColor(state: PluginState | undefined): string { + useEffect(() => { + PluginManagementEndpoint.validatePluginConfig(plugin.id).then((response: boolean) => { + if (response === undefined) return; + setConfigValid(response); + }); + }, []); + + function borderColor(state: PluginState | undefined): "success" | "warning" | "danger" | "default" { + if (configValid === undefined) return "default"; + if (!configValid) return "danger"; + return stateToColor(state); + } + + function stateToColor(state: PluginState | undefined): "success" | "warning" | "danger" | "default" { switch (state) { case PluginState.STARTED: return "success"; @@ -17,32 +32,33 @@ export function PluginManagementCard({plugin}: { plugin: PluginDto }) { case PluginState.STOPPED: return "danger"; default: - return ""; + return "default"; } } return ( <> - -
- - - -
-
-

{plugin.name}

-
- {plugin.version} -
-
-

Author: {plugin.author}

+ +
+ +

{plugin.name}

+
+ {plugin.version} + {plugin.state?.toLowerCase()} + {configValid === undefined ? + + : configValid ? + config valid : + config invalid + }
- diff --git a/gameyfin/src/main/frontend/components/general/UserManagementCard.tsx b/gameyfin/src/main/frontend/components/general/UserManagementCard.tsx index cde7fbb..a5bfab7 100644 --- a/gameyfin/src/main/frontend/components/general/UserManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/UserManagementCard.tsx @@ -11,7 +11,6 @@ import TokenDto from "Frontend/generated/de/grimsi/gameyfin/shared/token/TokenDt import UserInfoDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserInfoDto"; import RoleChip from "Frontend/components/general/RoleChip"; import AssignRolesModal from "Frontend/components/general/AssignRolesModal"; -import Role from "Frontend/generated/de/grimsi/gameyfin/core/Role"; export function UserManagementCard({user}: { user: UserInfoDto }) { const userDeletionConfirmationModal = useDisclosure(); @@ -123,7 +122,7 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {

{user.username}

{user.email}

{user.roles?.map((role) => ( - + ))}
diff --git a/gameyfin/src/main/frontend/views/MainLayout.tsx b/gameyfin/src/main/frontend/views/MainLayout.tsx index 639ca7f..05420e3 100644 --- a/gameyfin/src/main/frontend/views/MainLayout.tsx +++ b/gameyfin/src/main/frontend/views/MainLayout.tsx @@ -19,7 +19,7 @@ export default function MainLayout() { const [isExploding, setIsExploding] = useState(false); useEffect(() => { - let newTitle = `Gameyfin - ${routeMetadata?.title}` ?? 'Gameyfin'; + let newTitle = `Gameyfin - ${routeMetadata?.title}`; window.addEventListener('popstate', () => document.title = newTitle); loadUserTheme().catch(console.error); }, []); diff --git a/gameyfin/src/main/frontend/views/TestView.tsx b/gameyfin/src/main/frontend/views/TestView.tsx index 96879b0..e284a50 100644 --- a/gameyfin/src/main/frontend/views/TestView.tsx +++ b/gameyfin/src/main/frontend/views/TestView.tsx @@ -1,7 +1,7 @@ import {Link} from "react-router-dom"; import {Button, Input} from "@nextui-org/react"; import {toast} from "sonner"; -import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints.js"; +import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints"; import {useState} from "react"; import Game from "Frontend/generated/de/grimsi/gameyfin/games/Game"; diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt index 3c6fc8b..696ae1a 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt @@ -59,6 +59,14 @@ class GameyfinPluginManager( plugin.start() } + fun validatePluginConfig(pluginId: String): Boolean { + val plugin = getPlugin(pluginId)?.plugin ?: return false + if (plugin is GameyfinPlugin) { + return plugin.validateConfig() + } + return false + } + private fun configurePlugin(pluginWrapper: PluginWrapper) { val plugin = pluginWrapper.plugin if (plugin is GameyfinPlugin) { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEndpoint.kt index 86dcf0d..1687578 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEndpoint.kt @@ -20,4 +20,6 @@ class PluginManagementEndpoint( fun enablePlugin(pluginId: String) = pluginManagementService.enablePlugin(pluginId) fun disablePlugin(pluginId: String) = pluginManagementService.disablePlugin(pluginId) + + fun validatePluginConfig(pluginId: String): Boolean = pluginManagementService.validatePluginConfig(pluginId) } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt index 38cf7f9..0210dc0 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt @@ -37,4 +37,8 @@ class PluginManagementService( fun disablePlugin(pluginId: String) { pluginManager.disablePlugin(pluginId) } + + fun validatePluginConfig(pluginId: String): Boolean { + return pluginManager.validatePluginConfig(pluginId) + } } \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt index f7c1438..1138e4c 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt @@ -15,4 +15,10 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { open fun loadConfig(config: Map) { this.config = config } + + open fun validateConfig(): Boolean { + return validateConfig(config) + } + + abstract fun validateConfig(config: Map): Boolean } \ No newline at end of file diff --git a/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt b/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt index 3f7b26a..6783fa3 100644 --- a/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt +++ b/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt @@ -13,7 +13,6 @@ import me.xdrop.fuzzywuzzy.FuzzySearch import org.pf4j.Extension import org.pf4j.PluginWrapper import java.time.Instant -import kotlin.collections.filter class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { @@ -22,6 +21,16 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { PluginConfigElement("clientSecret", "Twitch client secret", "Your Twitch Client Secret") ) + override fun validateConfig(config: Map): Boolean { + try { + authenticate() + return true + } catch (e: PluginConfigError) { + log.error(e.message) + return false + } + } + override fun start() { try { authenticate() diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt index 7833c8d..4e2554c 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt @@ -4,19 +4,15 @@ import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.get -import io.ktor.serialization.kotlinx.json.json +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.boolean -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.* import me.xdrop.fuzzywuzzy.FuzzySearch import org.pf4j.Extension import org.pf4j.PluginWrapper @@ -26,7 +22,7 @@ import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset import java.time.format.DateTimeFormatter -import java.util.Locale +import java.util.* @Serializable data class SteamSearchResult( @@ -64,6 +60,10 @@ data class Platforms( class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { override val configMetadata: List = emptyList() + override fun validateConfig(config: Map): Boolean { + return true + } + @Extension class SteamMetadataProvider : GameMetadataProvider { val client = HttpClient(CIO) {