From f2fa014571cc96fb7e4209f5f2da7a060f23c320 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Fri, 16 May 2025 13:04:14 +0200 Subject: [PATCH] Sort plugins by type in frontend --- .../administration/PluginManagement.tsx | 59 +++--------------- .../general/PluginManagementSection.tsx | 61 +++++++++++++++++++ gameyfin/src/main/frontend/util/utils.ts | 6 ++ .../management/GameyfinPluginManager.kt | 15 +++++ .../management/PluginManagementEndpoint.kt | 6 +- .../management/PluginManagementService.kt | 23 ++++++- 6 files changed, 116 insertions(+), 54 deletions(-) create mode 100644 gameyfin/src/main/frontend/components/general/PluginManagementSection.tsx diff --git a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx index 4c17318..ce859e3 100644 --- a/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/PluginManagement.tsx @@ -1,31 +1,11 @@ -import React, {useEffect, useState} from "react"; -import {PluginManagementEndpoint} from "Frontend/generated/endpoints"; -import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto"; -import {PluginManagementCard} from "Frontend/components/general/cards/PluginManagementCard"; -import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react"; -import {ListNumbers} from "@phosphor-icons/react"; -import PluginPrioritiesModal from "Frontend/components/general/modals/PluginPrioritiesModal"; +import React from "react"; +import {Divider} from "@heroui/react"; +import {PluginManagementSection} from "Frontend/components/general/PluginManagementSection"; export default function PluginManagement() { - const [plugins, setPlugins] = useState([]); - const pluginPrioritiesModal = useDisclosure(); - useEffect(() => { - PluginManagementEndpoint.getPlugins().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)); - } + // Defined manually for now to control the layout (order of categories) + const pluginTypes = ["GameMetadataProvider", "DownloadProvider"]; return (
@@ -34,34 +14,11 @@ export default function PluginManagement() {
-
-

Metadata

- - - - -
- -
- {plugins.map((plugin) => +
+ {pluginTypes.map(type => + )}
- -
-

Notifications

-
-

Notification plugins not yet supported.

- - p.id + p.priority).join(',')} // force re-mount if plugin order changes - plugins={[...plugins].sort((a, b) => b.priority - a.priority)} - isOpen={pluginPrioritiesModal.isOpen} - onOpenChange={pluginPrioritiesModal.onOpenChange} - />
); } \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/PluginManagementSection.tsx b/gameyfin/src/main/frontend/components/general/PluginManagementSection.tsx new file mode 100644 index 0000000..129eb47 --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/PluginManagementSection.tsx @@ -0,0 +1,61 @@ +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 PluginPrioritiesModal from "Frontend/components/general/modals/PluginPrioritiesModal"; +import {camelCaseToTitle} from "Frontend/util/utils"; + +interface PluginManagementSectionProps { + pluginType: string; +} + +export function PluginManagementSection({pluginType}: PluginManagementSectionProps) { + const [plugins, setPlugins] = useState([]); + const pluginPrioritiesModal = useDisclosure(); + + useEffect(() => { + PluginManagementEndpoint.getPlugins(pluginType).then((response) => { + let sortedPlugins: PluginDto[] = response + .filter(p => !!p) + .sort((a: PluginDto, b: PluginDto) => { + if (a.name === undefined || b.name === undefined) return 0; + return a.name.localeCompare(b.name); + }); + + setPlugins(sortedPlugins); + }); + }, []); + + function updatePlugin(plugin: PluginDto) { + setPlugins(plugins.map(p => p.id === plugin.id ? plugin : p)); + } + + return ( +
+
+

{camelCaseToTitle(pluginType)}

+ + + + +
+ +
+ {plugins.map((plugin) => + )} +
+ + p.id + p.priority).join(',')} // force re-mount if plugin order changes + plugins={[...plugins].sort((a, b) => b.priority - a.priority)} + isOpen={pluginPrioritiesModal.isOpen} + onOpenChange={pluginPrioritiesModal.onOpenChange} + /> +
); +} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/util/utils.ts b/gameyfin/src/main/frontend/util/utils.ts index 44e3cb8..938444e 100644 --- a/gameyfin/src/main/frontend/util/utils.ts +++ b/gameyfin/src/main/frontend/util/utils.ts @@ -30,6 +30,12 @@ export function toTitleCase(str: string) { }).join(' '); } +export function camelCaseToTitle(text: string): string { + return text + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/^./, str => str.toUpperCase()); +} + export function roleToColor(role: string) { switch (role) { case "ROLE_SUPERADMIN": 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 0e5951a..397834f 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 @@ -175,6 +175,21 @@ class GameyfinPluginManager( return PluginConfigValidationResult.INVALID } + fun getExtensionTypeClasses(pluginId: String): Set> { + return getExtensionClasses(pluginId) + .flatMap { it.interfaces.toList() } + .filterIsInstance>() + .toSet() + } + + fun getExtensionTypes(pluginId: String): Set { + return getExtensionClasses(pluginId) + .flatMap { it.interfaces.toList() } + .filterIsInstance>() + .map { it.simpleName } + .toSet() + } + private fun configurePlugin(pluginWrapper: PluginWrapper) { val plugin = pluginWrapper.plugin if (plugin is Configurable) { 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 7c3f5a9..1914b2b 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 @@ -10,7 +10,11 @@ import jakarta.annotation.security.RolesAllowed class PluginManagementEndpoint( private val pluginManagementService: PluginManagementService ) { - fun getPlugins() = pluginManagementService.getPluginDtos() + fun getSupportedPluginTypes() = pluginManagementService.getSupportedPluginTypes() + + fun getPlugins(type: String?) = pluginManagementService.getPluginDtos(type) + + fun getPluginsMappedToTypes() = pluginManagementService.getPluginDtosMappedToTypes() fun getPlugin(pluginId: String) = pluginManagementService.getPluginDto(pluginId) 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 cfb3e71..cabfb9f 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 @@ -12,8 +12,27 @@ class PluginManagementService( private val pluginManager: GameyfinPluginManager, private val pluginManagementRepository: PluginManagementRepository, ) { - fun getPluginDtos(): List { - return pluginManager.plugins.map { toDto(it) } + + fun getSupportedPluginTypes(): Set { + return pluginManager.plugins + .flatMap { pluginManager.getExtensionTypes(it.pluginId) } + .toSet() + } + + fun getPluginDtos(type: String?): Set { + return pluginManager.plugins + .filter { type == null || type in pluginManager.getExtensionTypes(it.pluginId) } + .map { toDto(it) } + .toSet() + } + + fun getPluginDtosMappedToTypes(): Map> { + 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 {