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()
@@ -2,9 +2,15 @@ package de.grimsi.gameyfin.pluginapi.core
import org.pf4j.Plugin
import org.pf4j.PluginWrapper
import java.io.InputStream
abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
companion object {
const val LOGO_FILE_NAME: String = "logo"
val SUPPORTED_LOGO_FORMATS: List<String> = listOf("png", "jpg", "jpeg", "gif", "svg", "webp")
}
abstract val configMetadata: List<PluginConfigElement>
protected open var config: Map<String, String?> = emptyMap()
@@ -21,4 +27,28 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
}
abstract fun validateConfig(config: Map<String, String?>): Boolean
fun hasLogo(): Boolean {
for (format in SUPPORTED_LOGO_FORMATS) {
val resourcePath = "$LOGO_FILE_NAME.$format"
val inputStream = wrapper.pluginClassLoader.getResourceAsStream(resourcePath)
if (inputStream != null) {
return true
}
}
return false
}
fun getLogo(): InputStream? {
for (format in SUPPORTED_LOGO_FORMATS) {
val resourcePath = "$LOGO_FILE_NAME.$format"
val inputStream = wrapper.pluginClassLoader.getResourceAsStream(resourcePath)
if (inputStream != null) {
return inputStream
}
}
return null
}
}
+6
View File
@@ -25,6 +25,12 @@ subprojects {
}
from(sourceSets["main"].output.classesDirs)
from(sourceSets["main"].resources)
// Include logo file under META-INF/resources
from("src/main/resources") {
include("logo.*")
into("META-INF/resources")
}
}
tasks.register<Copy>("copyDependencyClasses") {
+1 -1
View File
@@ -2,5 +2,5 @@ Manifest-Version: 1.0
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
Plugin-Id: igdb
Plugin-Description: IGDB Metadata
Plugin-Version: 1.0.0-alpha1
Plugin-Version: 1.0.0-alpha2
Plugin-Provider: grimsi
+17
View File
@@ -0,0 +1,17 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 114 55" width="114" height="55">
<title>igdb_logo-svg</title>
<style>
.s0 { fill: #9147FF }
</style>
<g id="Layer_1">
<path id="Layer" class="s0" d="m12.7 12.3h5.3v23.9h-5.3z"/>
<path id="Layer" class="s0"
d="m24.2 24.3v-0.1c0-6.7 5.3-12.3 12.5-12.3 4.3 0 6.9 1.2 9.4 3.3l-3.3 4c-1.9-1.6-3.5-2.5-6.3-2.5-3.8 0-6.8 3.4-6.8 7.5 0 4.4 3 7.6 7.2 7.6 1.9 0 3.6-0.5 4.9-1.4v-3.4h-5.2v-4.6h10.3v10.4q-1 0.9-2.2 1.6-1.2 0.7-2.5 1.2-1.3 0.5-2.7 0.7-1.4 0.3-2.8 0.3c-7.4 0-12.5-5.2-12.5-12.3z"/>
<path id="Layer" fill-rule="evenodd" class="s0"
d="m53.3 12.3h9.3c7.5 0 12.6 5.2 12.6 11.9 0 6.8-5.1 12-12.6 12h-9.3zm5.2 4.7v14.4h4.1c4.3 0 7.2-2.9 7.2-7.1v-0.1c0-4.2-2.9-7.2-7.2-7.2z"/>
<path id="Layer" fill-rule="evenodd" class="s0"
d="m81.3 12.3h11c2.7 0 4.9 0.8 6.2 2.1q0.4 0.4 0.8 0.9 0.3 0.5 0.5 1 0.2 0.5 0.3 1.1 0.1 0.5 0.1 1.1v0.1c0 2.6-1.5 4.1-3.1 5.1 2.7 1.1 4.4 2.7 4.4 5.9 0 4.4-3.5 6.6-8.9 6.6h-11.3zm13.7 7c0-1.5-1.3-2.4-3.5-2.4h-5.1v5h4.8c2.3 0 3.8-0.7 3.8-2.5zm-2.6 6.9h-6v5.3h6.2c2.3 0 3.7-0.8 3.7-2.6v-0.1c0-1.6-1.2-2.6-3.9-2.6z"/>
<path id="Layer" fill-rule="evenodd" class="s0"
d="m114.2 55l-1.9-0.3q-13.7-2.2-27.5-3.3-13.8-1-27.7-1-13.8 0-27.7 1-13.8 1.1-27.5 3.3l-1.9 0.3v-55h114.2zm-57.1-8.1q6.8 0 13.5 0.3 6.8 0.3 13.5 0.8 6.7 0.5 13.4 1.3 6.7 0.7 13.4 1.7v-47.7h-107.5v47.7q6.6-1 13.3-1.7 6.7-0.8 13.5-1.3 6.7-0.5 13.4-0.8 6.8-0.3 13.5-0.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1 -1
View File
@@ -2,5 +2,5 @@ Manifest-Version: 1.0
Plugin-Class: de.grimsi.gameyfin.plugins.steam.SteamPlugin
Plugin-Id: steam
Plugin-Description: Steam Metadata
Plugin-Version: 1.0.0-alpha2
Plugin-Version: 1.0.0-alpha3
Plugin-Provider: grimsi
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" viewBox="0 0 512 512"><script xmlns=""/><symbol id="b" viewBox="-32 -32 64 64"><linearGradient id="a" x1="7762.648" x2="7762.648" y1="-8454.313" y2="-8453.313" gradientTransform="matrix(63.931 0 0 64 -496273.844 541044)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#111d2e"/><stop offset=".212" style="stop-color:#051839"/><stop offset=".407" style="stop-color:#0a1b48"/><stop offset=".581" style="stop-color:#132e62"/><stop offset=".738" style="stop-color:#144b7e"/><stop offset=".873" style="stop-color:#136497"/><stop offset="1" style="stop-color:#1387b8"/></linearGradient><path d="M-30.7 9.2C-26.7 22.4-14.5 32 0 32c17.7 0 32-14.3 32-32S17.7-32 0-32c-17 0-30.9 13.2-32 29.9 2.1 3.5 2.9 5.6 1.3 11.3" style="fill:url(#a)"/><path d="M-1.7-8v.2L-9.5 3.5c-1.3-.1-2.5.2-3.7.7-.5.2-1 .5-1.5.8l-17.2-7.1s-.4 6.5 1.3 11.4l12.2 5c.6 2.7 2.5 5.1 5.2 6.3 4.5 1.9 9.7-.3 11.6-4.8.5-1.2.7-2.4.7-3.7l11.2-8h.3c6.7 0 12.2-5.5 12.2-12.2s-5.4-12.2-12.2-12.2C3.8-20.2-1.7-14.7-1.7-8m-1.8 23c-1.5 3.5-5.5 5.1-9 3.7-1.5-.7-2.8-1.8-3.5-3.4l4 1.6c2.6 1.1 5.5-.1 6.6-2.7s-.1-5.5-2.7-6.6L-12.3 6c1.6-.6 3.4-.6 5 .1 1.7.7 3 2 3.7 3.7s.7 3.5.1 5.2M10.5.1C6 .1 2.4-3.5 2.4-8s3.6-8.1 8.1-8.1 8.1 3.6 8.1 8.1S15 .1 10.5.1M4.4-8c0-3.4 2.7-6.1 6.1-6.1s6.1 2.7 6.1 6.1-2.7 6.1-6.1 6.1S4.4-4.7 4.4-8" style="fill:#fff"/></symbol><use xlink:href="#b" width="64" height="64" x="-32" y="-32" style="overflow:visible" transform="matrix(8 0 0 8 256 256)"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB