Sort plugins by type in frontend

This commit is contained in:
grimsi
2025-05-16 13:04:14 +02:00
parent 9c6becb29e
commit f2fa014571
6 changed files with 116 additions and 54 deletions
@@ -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<PluginDto[]>([]);
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 (
<div className="flex flex-col">
@@ -34,34 +14,11 @@ export default function PluginManagement() {
</div>
<Divider className="mb-4"/>
<div className="flex flex-row flex-grow justify-between mb-8">
<h2 className="text-xl font-bold">Metadata</h2>
<Tooltip color="foreground" placement="left" content="Change plugin order">
<Button isIconOnly variant="flat" onPress={pluginPrioritiesModal.onOpen}>
<ListNumbers/>
</Button>
</Tooltip>
</div>
<div className="grid grid-cols-300px gap-4">
{plugins.map((plugin) => <PluginManagementCard plugin={plugin}
updatePlugin={updatePlugin}
key={plugin.name}/>
<div className="flex flex-col gap-8">
{pluginTypes.map(type =>
<PluginManagementSection key={type} pluginType={type}/>
)}
</div>
<div className="flex flex-row flex-grow justify-between my-8">
<h2 className="text-xl font-bold">Notifications</h2>
</div>
<p>Notification plugins not yet supported.</p>
<PluginPrioritiesModal
key={plugins.map(p => 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}
/>
</div>
);
}
@@ -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<PluginDto[]>([]);
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>
<Tooltip color="foreground" placement="left" content="Change plugin order">
<Button isIconOnly variant="flat" onPress={pluginPrioritiesModal.onOpen}>
<ListNumbers/>
</Button>
</Tooltip>
</div>
<div className="grid grid-cols-300px gap-4">
{plugins.map((plugin) => <PluginManagementCard plugin={plugin}
updatePlugin={updatePlugin}
key={plugin.name}/>
)}
</div>
<PluginPrioritiesModal
key={plugins.map(p => 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}
/>
</div>);
}
+6
View File
@@ -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":
@@ -175,6 +175,21 @@ class GameyfinPluginManager(
return PluginConfigValidationResult.INVALID
}
fun getExtensionTypeClasses(pluginId: String): Set<Class<ExtensionPoint>> {
return getExtensionClasses(pluginId)
.flatMap { it.interfaces.toList() }
.filterIsInstance<Class<ExtensionPoint>>()
.toSet()
}
fun getExtensionTypes(pluginId: String): Set<String> {
return getExtensionClasses(pluginId)
.flatMap { it.interfaces.toList() }
.filterIsInstance<Class<ExtensionPoint>>()
.map { it.simpleName }
.toSet()
}
private fun configurePlugin(pluginWrapper: PluginWrapper) {
val plugin = pluginWrapper.plugin
if (plugin is Configurable) {
@@ -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)
@@ -12,8 +12,27 @@ class PluginManagementService(
private val pluginManager: GameyfinPluginManager,
private val pluginManagementRepository: PluginManagementRepository,
) {
fun getPluginDtos(): List<PluginDto> {
return pluginManager.plugins.map { toDto(it) }
fun getSupportedPluginTypes(): Set<String> {
return pluginManager.plugins
.flatMap { pluginManager.getExtensionTypes(it.pluginId) }
.toSet()
}
fun getPluginDtos(type: String?): Set<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 {