diff --git a/gameyfin/build.gradle.kts b/gameyfin/build.gradle.kts index dee05aa..9d0435b 100644 --- a/gameyfin/build.gradle.kts +++ b/gameyfin/build.gradle.kts @@ -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") diff --git a/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx b/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx index ad8a012..16583aa 100644 --- a/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginDetailsModal.tsx @@ -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

Details

- +

Author: {plugin.author}

Version: {plugin.version}

diff --git a/gameyfin/src/main/frontend/components/general/PluginLogo.tsx b/gameyfin/src/main/frontend/components/general/PluginLogo.tsx new file mode 100644 index 0000000..fe61b1f --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/PluginLogo.tsx @@ -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 ? + : + + } + + ); +} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx b/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx index 7bfad4f..d39da76 100644 --- a/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginManagementCard.tsx @@ -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 ; + case PluginState.DISABLED: + return ; + case PluginState.FAILED: + return ; + default: + return ; + } + } + function isDisabled(state: PluginState | undefined): boolean { return state === PluginState.DISABLED; } @@ -62,6 +85,7 @@ export function PluginManagementCard({plugin, updatePlugin}: { } } + // @ts-ignore return ( <> @@ -79,17 +103,29 @@ export function PluginManagementCard({plugin, updatePlugin}: {
- +

{plugin.name}

{plugin.version} - {plugin.state?.toLowerCase()} + + + {stateToIcon(plugin.state)} + + {configValid === undefined ? - + : configValid ? - config valid : - config invalid + + + + + : + + + + + }
diff --git a/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.css b/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.css deleted file mode 100644 index 1057265..0000000 --- a/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.css +++ /dev/null @@ -1,9 +0,0 @@ -.react-aria-ListBoxItem { - &[data-dragging] { - opacity: 0.6; - } -} - -.react-aria-DropIndicator[data-drop-target] { - outline: 1px solid theme('colors.primary'); -} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.tsx b/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.tsx index ae36d88..82a9c56 100644 --- a/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.tsx +++ b/gameyfin/src/main/frontend/components/general/PluginPrioritiesModal.tsx @@ -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[]; diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt index fa9e9f0..e988d1c 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt @@ -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 { + 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) + } } } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginDto.kt index 70fa130..603e6b5 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginDto.kt @@ -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 ) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt index 8e068a5..26598be 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementService.kt @@ -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() + } } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt index ba460fc..1d2a892 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/media/ImageEndpoint.kt @@ -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? { + val logo = pluginManagementService.getLogo(pluginId) + return Utils.inputStreamToResponseEntity(logo) + } + @GetMapping("/avatar") fun getAvatarByUsername(@RequestParam username: String): ResponseEntity? { val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build() diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt index 1138e4c..ffc60a0 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt @@ -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 = listOf("png", "jpg", "jpeg", "gif", "svg", "webp") + } + abstract val configMetadata: List protected open var config: Map = emptyMap() @@ -21,4 +27,28 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { } abstract fun validateConfig(config: Map): 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 + } } \ No newline at end of file diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index bca916d..0b7e3ca 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -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("copyDependencyClasses") { diff --git a/plugins/igdb/src/main/resources/MANIFEST.MF b/plugins/igdb/src/main/resources/MANIFEST.MF index fdcf8d8..ba33b38 100644 --- a/plugins/igdb/src/main/resources/MANIFEST.MF +++ b/plugins/igdb/src/main/resources/MANIFEST.MF @@ -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 diff --git a/plugins/igdb/src/main/resources/logo.svg b/plugins/igdb/src/main/resources/logo.svg new file mode 100644 index 0000000..4504ba4 --- /dev/null +++ b/plugins/igdb/src/main/resources/logo.svg @@ -0,0 +1,17 @@ + + igdb_logo-svg + + + + + + + + + \ No newline at end of file diff --git a/plugins/steam/src/main/resources/MANIFEST.MF b/plugins/steam/src/main/resources/MANIFEST.MF index 414fb2d..953a2f4 100644 --- a/plugins/steam/src/main/resources/MANIFEST.MF +++ b/plugins/steam/src/main/resources/MANIFEST.MF @@ -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 diff --git a/plugins/steam/src/main/resources/logo.svg b/plugins/steam/src/main/resources/logo.svg new file mode 100644 index 0000000..4ca6ab0 --- /dev/null +++ b/plugins/steam/src/main/resources/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file