Add logo support to plugins and UI

This commit is contained in:
grimsi
2025-03-30 12:11:58 +02:00
parent 55818d4f37
commit 17d3211d22
16 changed files with 172 additions and 23 deletions
+1
View File
@@ -55,6 +55,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.15")
implementation("commons-io:commons-io:2.18.0")
implementation("org.apache.tika:tika-core:3.1.0")
// SSO
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
@@ -5,7 +5,7 @@ import {PluginConfigEndpoint, PluginManagementEndpoint} from "Frontend/generated
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 {Plug} from "@phosphor-icons/react";
import PluginLogo from "Frontend/components/general/PluginLogo";
interface PluginDetailsModalProps {
plugin: PluginDto;
@@ -59,7 +59,7 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
<ModalBody>
<h4 className="text-l font-bold">Details</h4>
<div className="flex flex-row gap-8">
<Plug size={64} weight="fill"/>
<PluginLogo plugin={plugin}/>
<div className="grid grid-cols-2">
<p>Author: {plugin.author}</p>
<p>Version: {plugin.version}</p>
@@ -0,0 +1,19 @@
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";
interface PluginLogoProps {
plugin: PluginDto;
}
export default function PluginLogo({plugin}: PluginLogoProps) {
return (
<>
{plugin.hasLogo ?
<Image src={`/images/plugins/${plugin.id}/logo`} width={64} height={64} radius="none"/> :
<Plug size={64} weight="fill"/>
}
</>
);
}
@@ -1,10 +1,20 @@
import {Button, Card, Chip, Skeleton, Tooltip, useDisclosure} from "@heroui/react";
import {Plug, Power, SlidersHorizontal} from "@phosphor-icons/react";
import {
CheckCircle,
PauseCircle,
PlayCircle,
Power,
QuestionMark,
SlidersHorizontal,
StopCircle,
WarningCircle
} 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, {useEffect, useState} from "react";
import React, {ReactNode, useEffect, useState} from "react";
import PluginDetailsModal from "Frontend/components/general/PluginDetailsModal";
import PluginLogo from "Frontend/components/general/PluginLogo";
export function PluginManagementCard({plugin, updatePlugin}: {
plugin: PluginDto,
@@ -40,6 +50,19 @@ export function PluginManagementCard({plugin, updatePlugin}: {
}
}
function stateToIcon(state: PluginState | undefined): ReactNode {
switch (state) {
case PluginState.STARTED:
return <PlayCircle/>;
case PluginState.DISABLED:
return <PauseCircle/>;
case PluginState.FAILED:
return <StopCircle/>;
default:
return <QuestionMark/>;
}
}
function isDisabled(state: PluginState | undefined): boolean {
return state === PluginState.DISABLED;
}
@@ -62,6 +85,7 @@ export function PluginManagementCard({plugin, updatePlugin}: {
}
}
// @ts-ignore
return (
<>
<Card className={`flex flex-row justify-between p-2 border-2 border-${borderColor(plugin.state)}`}>
@@ -79,17 +103,29 @@ export function PluginManagementCard({plugin, updatePlugin}: {
</Tooltip>
</div>
<div className="flex flex-1 flex-col items-center gap-1">
<Plug size={64} weight="fill"/>
<PluginLogo plugin={plugin}/>
<p className="font-semibold">{plugin.name}</p>
<div className="flex flex-row gap-2">
<Chip size="sm" radius="sm" className="text-xs">{plugin.version}</Chip>
<Chip size="sm" radius="sm" className="text-xs"
color={stateToColor(plugin.state)}>{plugin.state?.toLowerCase()}</Chip>
<Chip size="sm" radius="sm" className="text-xs" color={stateToColor(plugin.state)}>
<Tooltip content={`Plugin ${plugin.state?.toLowerCase()}`} placement="bottom"
color="foreground">
{stateToIcon(plugin.state)}
</Tooltip>
</Chip>
{configValid === undefined ?
<Skeleton className="rounded-md h-6 w-20"></Skeleton>
<Skeleton className="rounded-md h-6 w-9"/>
: configValid ?
<Chip size="sm" radius="sm" className="text-xs" color="success">config valid</Chip> :
<Chip size="sm" radius="sm" className="text-xs" color="danger">config invalid</Chip>
<Tooltip content="Config valid" placement="bottom" color="foreground">
<Chip size="sm" radius="sm" className="text-xs" color="success">
<CheckCircle/>
</Chip>
</Tooltip> :
<Tooltip content="Config invalid" placement="bottom" color="foreground">
<Chip size="sm" radius="sm" className="text-xs" color="danger">
<WarningCircle/>
</Chip>
</Tooltip>
}
</div>
</div>
@@ -1,9 +0,0 @@
.react-aria-ListBoxItem {
&[data-dragging] {
opacity: 0.6;
}
}
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid theme('colors.primary');
}
@@ -5,7 +5,6 @@ import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/manage
import {ListBox, ListBoxItem, useDragAndDrop} from "react-aria-components";
import {CaretUpDown} from "@phosphor-icons/react";
import {ListData, useListData} from "@react-stately/data";
import './PluginPrioritiesModal.css';
interface PluginPrioritiesModalProps {
plugins: PluginDto[];
@@ -1,10 +1,18 @@
package de.grimsi.gameyfin.core
import org.apache.tika.Tika
import org.springframework.core.io.InputStreamResource
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.io.InputStream
class Utils {
companion object {
private val tika = Tika()
fun maskEmail(email: String): String {
val regex = """(?:\G(?!^)|(?<=^[^@]{2}|@))[^@](?!\.[^.]+$)""".toRegex()
return email.replace(regex, "*")
@@ -22,5 +30,22 @@ class Utils {
"$scheme://$serverName:$serverPort"
}
}
fun inputStreamToResponseEntity(stream: InputStream?): ResponseEntity<InputStreamResource> {
if (stream == null) return ResponseEntity.notFound().build()
val inputStreamResource = InputStreamResource(stream)
val headers = HttpHeaders()
val contentLength = stream.available().toLong()
val contentType = tika.detect(stream)
headers.contentLength = contentLength
headers.contentType = MediaType.parseMediaType(contentType)
return ResponseEntity.ok()
.headers(headers)
.body(inputStreamResource)
}
}
}
@@ -7,6 +7,7 @@ data class PluginDto(
val name: String,
val version: String,
val author: String,
val hasLogo: Boolean,
val state: PluginState,
val priority: Int
)
@@ -1,8 +1,10 @@
package de.grimsi.gameyfin.core.plugins.management
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
import org.pf4j.ExtensionPoint
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.io.InputStream
@Service
class PluginManagementService(
@@ -16,6 +18,7 @@ class PluginManagementService(
it.descriptor.pluginDescription,
it.descriptor.version,
it.descriptor.provider,
(it.plugin as GameyfinPlugin).hasLogo(),
it.pluginState,
getPluginManagementEntry(it.pluginId).priority
)
@@ -29,6 +32,7 @@ class PluginManagementService(
plugin.descriptor.pluginDescription,
plugin.descriptor.version,
plugin.descriptor.provider,
(plugin.plugin as GameyfinPlugin).hasLogo(),
plugin.pluginState,
getPluginManagementEntry(pluginId).priority
)
@@ -82,4 +86,14 @@ class PluginManagementService(
pluginManagementRepository.save(pluginManagementEntry)
}
}
fun hasLogo(pluginId: String): Boolean {
val plugin = pluginManager.getPlugin(pluginId).plugin as GameyfinPlugin
return plugin.hasLogo()
}
fun getLogo(pluginId: String): InputStream? {
val plugin = pluginManager.getPlugin(pluginId).plugin as GameyfinPlugin
return plugin.getLogo()
}
}
@@ -1,7 +1,9 @@
package de.grimsi.gameyfin.media
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.core.Utils
import de.grimsi.gameyfin.core.annotations.DynamicPublicAccess
import de.grimsi.gameyfin.core.plugins.management.PluginManagementService
import de.grimsi.gameyfin.games.entities.Image
import de.grimsi.gameyfin.games.entities.ImageType
import de.grimsi.gameyfin.users.UserService
@@ -20,7 +22,8 @@ import org.springframework.web.multipart.MultipartFile
@RequestMapping("/images")
class ImageEndpoint(
private val imageService: ImageService,
private val userService: UserService
private val userService: UserService,
private val pluginManagementService: PluginManagementService
) {
@GetMapping("/screenshot/{id}")
@@ -33,6 +36,12 @@ class ImageEndpoint(
return getImageContent(id)
}
@GetMapping("/plugins/{id}/logo")
fun getPluginLogo(@PathVariable("id") pluginId: String): ResponseEntity<InputStreamResource>? {
val logo = pluginManagementService.getLogo(pluginId)
return Utils.inputStreamToResponseEntity(logo)
}
@GetMapping("/avatar")
fun getAvatarByUsername(@RequestParam username: String): ResponseEntity<InputStreamResource>? {
val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build()