mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Add logo support to plugins and UI
This commit is contained in:
@@ -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
|
||||
)
|
||||
+14
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user