mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 08:15:44 +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()
|
||||
|
||||
Reference in New Issue
Block a user