Release 2.4.0 (#870)

* chore: bump version to v2.4.0-preview

* Bump actions/cache from 4 to 5 (#865)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Increase maximum DB connection pool size (#876)

Increase DB connection timeout

* Disable length limit for DB field PLUGIN_CONFIG.value (#875)

* Bump actions/cache from 4 to 5 (#871)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/download-artifact from 7 to 8 (#882)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 6 to 7 (#881)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#878)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Dont perform scans if no metadata plugins are enabled (#877)

* Dont perform scans if no metadata plugins are enabled

* Fix tests

* Add PluginServiceTest.kt

* Fix Sonar finding

* Fix malformed external links (#886)

* Fix external links being treated as internal

* chore: bump version to v856-malformed-external-links-preview

* Update JVM in Dockerfile to Java 25

* Revert incorrect version update

* Allow loading .jar plugins in development mode (#885)

* Allow loading .jar plugins in development mode

* Remove unnecessary mock

* Fix unit test

* Add unit tests

* Fix/879 add info and reset to config options (#887)

* Fix gog.sh script

* Add "description" property to ConfigProperties.kt
Add InfoPopup.tsx and ResetToDefaultButton.tsx in UI

* Bump actions/download-artifact from 7 to 8 (#891)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#890)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 6 to 7 (#889)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Improve memory usage and performance (#888)

mprove memory usage and performance by:
* Using AOT cache
* Using tuned JAVA_OPTIONS
* Session timeout
* Jetty threadpool
* DB batch size
* DB pool size
* Library scanning
* Make scan-concurrency configurable
* Log retention
* Off-load image processing to disk instead of RAM

* Fix bug in PluginState

* Update dependency version for ksp

* Fix race condition preventing plugins from starting

* Show remaining time (estimation) for library scans

* Add unit test for plugin loading bugfix

* Add unit tests for ImageService calculateBlurHash

* Make username claim configurable (#895)

Add fallbacks to resolve username

* Fix sonar issues (#894)

* Add custom "/sonar" command to GH copilot

* Add Sonar plugin integration

* Fix issues reported by Sonar

* Ignore Sonar warning about AES/ECB

* Add unit tests for GameyfinPluginManager

* Add unit tests for GameService

* Add more unit tests for GameService

* Improve library card layout (#896)

* Fix title not being centered

* Add buttons to scan all libraries

* Disable AVX for AOT cache training

* Improve AOT cache training

* Fix tests

* Change output type of Docker Build CI action

* Increase MAX_WAIT of aot-training to 5min

* Optimize Docker CI pipeline

* Add Sonar badges to README.md

* Add custom metrics (downloads & scans)

* Optimize DB connection & add cache for images

* Adjusted logging on startup

* * Show message on start page when no libraries/games are available
* Disable "Scan" buttons when no metadata plugin is enabled

* Fix thread pinning causing deadlocks

* Pre-populate image cache at startup

* Show "Loading" spinner while loading

* Optimize static file serving (images)

* Switch back to Tomcat (from Jetty)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Simon
2026-03-13 15:34:06 +01:00
committed by GitHub
parent ecd369cd30
commit 3a932d953f
123 changed files with 6169 additions and 2003 deletions
@@ -15,7 +15,8 @@ import {libraryState} from "Frontend/state/LibraryState";
import {TargetIcon, WarningIcon} from "@phosphor-icons/react";
import {timeBetween, timeUntil, toTitleCase} from "Frontend/util/utils";
import LibraryScanStatus from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryScanStatus";
import {useEffect, useState} from "react";
import type LibraryScanProgress from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryScanProgress";
import {useEffect, useRef, useState} from "react";
export default function ScanProgressPopover() {
const libraries = useSnapshot(libraryState).state;
@@ -23,7 +24,10 @@ export default function ScanProgressPopover() {
const scanInProgress = useSnapshot(scanState).isScanning;
// Add state to track current time and force re-renders
const [currentTime, setCurrentTime] = useState(Date.now());
const [_currentTime, setCurrentTime] = useState(Date.now());
// Cache ETAs per scanId: { eta: string | null, computedAt: number }
const etaCacheRef = useRef<Record<string, { eta: string | null; computedAt: number }>>({});
// Set up an interval to update the time every second
useEffect(() => {
@@ -35,6 +39,42 @@ export default function ScanProgressPopover() {
return () => clearInterval(intervalId);
}, []);
function estimateTimeLeft(scan: LibraryScanProgress): string | null {
const now = Date.now();
const cached = etaCacheRef.current[scan.scanId];
// Only recompute every 5 seconds
if (cached && now - cached.computedAt < 5000) {
return cached.eta;
}
const current = scan.currentStep.current;
const total = scan.currentStep.total;
if (!current || !total || current <= 0 || total <= 0) {
etaCacheRef.current[scan.scanId] = {eta: null, computedAt: now};
return null;
}
const elapsed = (now - new Date(scan.startedAt).getTime()) / 1000;
if (elapsed <= 0) {
etaCacheRef.current[scan.scanId] = {eta: null, computedAt: now};
return null;
}
const rate = current / elapsed; // items per second
const remaining = total - current;
const secondsLeft = Math.round(remaining / rate);
if (secondsLeft < 0) {
etaCacheRef.current[scan.scanId] = {eta: null, computedAt: now};
return null;
}
const mins = Math.floor(secondsLeft / 60);
const secs = secondsLeft % 60;
const eta = `${mins}:${secs.toString().padStart(2, "0")} min left`;
etaCacheRef.current[scan.scanId] = {eta, computedAt: now};
return eta;
}
return (
<Popover placement="bottom-end" showArrow={true}>
<PopoverTrigger>
@@ -79,9 +119,14 @@ export default function ScanProgressPopover() {
{scan.status === LibraryScanStatus.IN_PROGRESS &&
(scan.currentStep.current && scan.currentStep.total ?
<div className="flex flex-col gap-1">
<p className="text-default-500">
{`${scan.currentStep.description} (${scan.currentStep.current}/${scan.currentStep.total})`}
</p>
<div className="flex flex-row justify-between">
<p className="text-default-500">
{`${scan.currentStep.description} (${scan.currentStep.current}/${scan.currentStep.total})`}
</p>
<p className="text-default-500">
{estimateTimeLeft(scan)}
</p>
</div>
<Progress
value={scan.currentStep.current / scan.currentStep.total * 100}
size="sm"/>
@@ -10,6 +10,7 @@ import {gameState} from "Frontend/state/GameState";
import IconBackgroundPattern from "Frontend/components/general/IconBackgroundPattern";
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
import ChipList from "Frontend/components/general/ChipList";
import {pluginState} from "Frontend/state/PluginState";
interface LibraryOverviewCardProps {
library: LibraryAdminDto;
@@ -20,6 +21,7 @@ export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
const navigate = useNavigate();
const state = useSnapshot(gameState);
const randomGames = getRandomGames();
const hasActiveMetadataPlugins = useSnapshot(pluginState).hasActiveMetadataPlugins;
function getRandomGames() {
if (!state.randomlyOrderedGamesByLibraryId[library.id]) return [];
@@ -47,16 +49,20 @@ export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
}
</div>
<p className="absolute text-2xl font-bold">{library.name}</p>
<p className="mt-6 absolute text-2xl text-center font-bold">{library.name}</p>
<div className="absolute right-0 top-0 flex flex-row">
<Tooltip content="Scan library (quick)" placement="bottom" color="foreground">
<Button isIconOnly variant="light" onPress={() => triggerScan(ScanType.QUICK)}>
<Button isIconOnly variant="light"
isDisabled={!hasActiveMetadataPlugins}
onPress={() => triggerScan(ScanType.QUICK)}>
<MagnifyingGlassIcon/>
</Button>
</Tooltip>
<Tooltip content="Scan library (full)" placement="bottom" color="foreground">
<Button isIconOnly variant="light" onPress={() => triggerScan(ScanType.FULL)}>
<Button isIconOnly variant="light"
isDisabled={!hasActiveMetadataPlugins}
onPress={() => triggerScan(ScanType.FULL)}>
<MagnifyingGlassPlusIcon/>
</Button>
</Tooltip>
@@ -2,11 +2,20 @@ import {FieldArray, useField} from "formik";
import {Button, Chip, Input, Popover, PopoverContent, PopoverTrigger} from "@heroui/react";
import {KeyboardEvent, useState} from "react";
import {PlusIcon} from "@phosphor-icons/react";
import InfoPopup from "Frontend/components/administration/InfoPopup";
import ResetToDefaultButton from "Frontend/components/administration/ResetToDefaultButton";
// @ts-ignore
const ArrayInput = ({label, ...props}) => {
// @ts-ignore
const [field, meta] = useField(props);
interface ArrayInputProps {
label: string;
name: string;
type?: string;
description?: string;
resetValue?: unknown;
isDisabled?: boolean;
}
export default function ArrayInput({label, description, resetValue, ...props}: ArrayInputProps) {
const [field, meta] = useField<string[]>(props.name);
const [newElementValue, setNewElementValue] = useState<string>("");
return (
@@ -29,12 +38,17 @@ const ArrayInput = ({label, ...props}) => {
return (
<div className="flex flex-col flex-1 gap-2">
<div className="flex flex-row justify-between">
<p>{label}</p>
<span className="flex items-center gap-1">
<p>{label}</p>
{description && <InfoPopup content={description}/>}
{resetValue !== undefined &&
<ResetToDefaultButton fieldName={field.name} defaultValue={resetValue}/>}
</span>
<small>{field.value.length} {field.value.length == 1 ? "element" : "elements"}</small>
</div>
<div className="flex flex-row flex-wrap gap-2 items-center">
{field.value.map((element: any, index: number) => (
{field.value.map((element: string, index: number) => (
<Chip key={index}
onClose={() => arrayHelpers.remove(index)}
isDisabled={props.isDisabled}
@@ -76,5 +90,3 @@ const ArrayInput = ({label, ...props}) => {
/>
);
}
export default ArrayInput;
@@ -1,29 +1,39 @@
import {useField} from "formik";
import {Checkbox, CheckboxGroup} from "@heroui/react";
import {Checkbox, CheckboxGroup, CheckboxProps} from "@heroui/react";
import InfoPopup from "Frontend/components/administration/InfoPopup";
import ResetToDefaultButton from "Frontend/components/administration/ResetToDefaultButton";
// @ts-ignore
const CheckboxInput = ({label, ...props}) => {
// @ts-ignore
const [field, meta] = useField(props);
interface CheckboxInputProps extends Omit<CheckboxProps, "name"> {
label: string;
name: string;
description?: string;
resetValue?: unknown;
}
export default function CheckboxInput({label, description, resetValue, className, ...props}: CheckboxInputProps) {
const [field, meta] = useField({name: props.name, type: "checkbox"});
return (
<CheckboxGroup
className="flex flex-row flex-1 items-baseline gap-2"
className={`flex flex-row flex-1 gap-2 ${className ?? ""}`}
isInvalid={!!meta.error}
errorMessage={meta.initialError || meta.error}
value={field.value ? [field.name] : []}
>
<Checkbox
className="items-baseline"
{...field}
{...props}
// @ts-ignore
value={field.name}
>
{label}
</Checkbox>
<span className="flex items-center gap-1">
<Checkbox
{...field}
{...props}
className="items-center"
value={field.name}
>
{label}
</Checkbox>
{description && <InfoPopup content={description}/>}
{resetValue !== undefined &&
<ResetToDefaultButton fieldName={field.name} defaultValue={resetValue}/>}
</span>
</CheckboxGroup>
);
}
export default CheckboxInput;
@@ -1,12 +1,15 @@
import {useField} from "formik";
import {DatePicker, DateValue} from "@heroui/react";
import {DatePicker, DatePickerProps, DateValue} from "@heroui/react";
import {parseDate} from "@internationalized/date";
import {useState} from "react";
// @ts-ignore
export default function DatePickerInput({label, showErrorUntouched = false, ...props}) {
// @ts-ignore
const [field, meta] = useField(props);
interface DatePickerInputProps extends Omit<DatePickerProps, "name"> {
name: string;
showErrorUntouched?: boolean;
}
export default function DatePickerInput({label, showErrorUntouched = false, ...props}: DatePickerInputProps) {
const [field, meta] = useField(props.name);
const [value, setValue] = useState<DateValue | null>(field.value ? parseDate(field.value) : null);
return (
@@ -26,7 +29,7 @@ export default function DatePickerInput({label, showErrorUntouched = false, ...p
}
});
}}
id={label}
id={label as string}
label={label}
isInvalid={(meta.touched || showErrorUntouched) && !!meta.error}
errorMessage={meta.initialError || meta.error}
@@ -3,13 +3,17 @@ import React from "react";
import {useField} from "formik";
import {GameCoverPickerModal} from "Frontend/components/general/modals/GameCoverPickerModal";
import {ImageBrokenIcon, PencilIcon} from "@phosphor-icons/react";
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
interface GameCoverPickerProps {
game: GameDto;
name: string;
showErrorUntouched?: boolean;
}
// @ts-ignore
export default function GameCoverPicker({game, showErrorUntouched = false, ...props}) {
export default function GameCoverPicker({game, showErrorUntouched = false, ...props}: GameCoverPickerProps) {
// @ts-ignore
const [field] = useField(props);
const [field] = useField(props.name);
const gameCoverPickerModal = useDisclosure();
@@ -3,13 +3,17 @@ import React from "react";
import {useField} from "formik";
import {ImageBrokenIcon, PencilIcon} from "@phosphor-icons/react";
import {GameHeaderPickerModal} from "Frontend/components/general/modals/GameHeaderPickerModal";
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
interface GameHeaderPickerProps {
game: GameDto;
name: string;
showErrorUntouched?: boolean;
}
// @ts-ignore
export default function GameHeaderPicker({game, showErrorUntouched = false, ...props}) {
export default function GameHeaderPicker({game, showErrorUntouched = false, ...props}: GameHeaderPickerProps) {
// @ts-ignore
const [field] = useField(props);
const [field] = useField(props.name);
const gameHeaderPickerModal = useDisclosure();
@@ -1,23 +1,43 @@
import {useField} from "formik";
import {Input as HeroUiInput} from "@heroui/react";
import {Input as HeroUiInput, InputProps} from "@heroui/react";
import InfoPopup from "Frontend/components/administration/InfoPopup";
import ResetToDefaultButton from "Frontend/components/administration/ResetToDefaultButton";
// @ts-ignore
const Input = ({label, showErrorUntouched = false, ...props}) => {
// @ts-ignore
const [field, meta] = useField(props);
interface CustomInputProps extends Omit<InputProps, "name"> {
name: string;
showErrorUntouched?: boolean;
resetValue?: unknown;
}
export default function Input({
label,
showErrorUntouched = false,
description,
className,
resetValue,
...props
}: CustomInputProps) {
const [field, meta] = useField(props.name);
return (
<HeroUiInput
className="min-h-20 grow"
fullWidth={false}
{...props}
{...field}
id={label}
className={`min-h-20 grow ${className ?? ""}`}
id={label as string}
label={label}
endContent={
(description || resetValue !== undefined) ? (
<span className="flex items-center gap-1">
{description && <InfoPopup content={description as string}/>}
{resetValue !== undefined &&
<ResetToDefaultButton fieldName={field.name} defaultValue={resetValue}/>}
</span>
) : undefined
}
isInvalid={(meta.touched || showErrorUntouched) && !!meta.error}
errorMessage={meta.initialError || meta.error}
/>
);
}
export default Input;
}
@@ -1,26 +1,46 @@
import {useField} from "formik";
import {NumberInput as HeroUiNumberInput} from "@heroui/react";
import {NumberInput as HeroUiNumberInput, NumberInputProps} from "@heroui/react";
import InfoPopup from "Frontend/components/administration/InfoPopup";
import ResetToDefaultButton from "Frontend/components/administration/ResetToDefaultButton";
// @ts-ignore
const NumberInput = ({label, showErrorUntouched = false, ...props}) => {
// @ts-ignore
const [field, meta, helpers] = useField(props);
interface CustomNumberInputProps extends Omit<NumberInputProps, "name"> {
name: string;
showErrorUntouched?: boolean;
resetValue?: unknown;
}
export default function NumberInput({
label,
showErrorUntouched = false,
description,
className,
resetValue,
...props
}: CustomNumberInputProps) {
const [field, meta, helpers] = useField<number>(props.name);
return (
<HeroUiNumberInput
className="min-h-20 grow"
fullWidth={false}
{...props}
className={`min-h-20 grow ${className ?? ""}`}
value={field.value}
onValueChange={(value) => helpers.setValue(value)}
onBlur={field.onBlur}
name={field.name}
id={label}
id={label as string}
label={label}
endContent={
(description || resetValue !== undefined) ? (
<span className="flex items-center gap-1">
{description && <InfoPopup content={description as string}/>}
{resetValue !== undefined &&
<ResetToDefaultButton fieldName={field.name} defaultValue={resetValue}/>}
</span>
) : undefined
}
isInvalid={(meta.touched || showErrorUntouched) && !!meta.error}
errorMessage={meta.initialError || meta.error}
/>
);
}
export default NumberInput;
@@ -1,10 +1,18 @@
import {useField} from "formik";
import {Select, SelectItem} from "@heroui/react";
import {Select, SelectItem, SelectProps} from "@heroui/react";
import InfoPopup from "Frontend/components/administration/InfoPopup";
import ResetToDefaultButton from "Frontend/components/administration/ResetToDefaultButton";
// @ts-ignore
const SelectInput = ({label, values, ...props}) => {
// @ts-ignore
const [field, meta] = useField(props);
interface SelectInputProps extends Omit<SelectProps, "name" | "children"> {
label: string;
name: string;
values: string[];
description?: string;
resetValue?: unknown;
}
export default function SelectInput({label, values, description, resetValue, ...props}: SelectInputProps) {
const [field, meta] = useField(props.name);
const items = values.map((v: string) => ({key: v, label: v}));
@@ -17,6 +25,15 @@ const SelectInput = ({label, values, ...props}) => {
label={label}
items={items}
selectedKeys={[field.value]}
endContent={
(description || resetValue !== undefined) ? (
<span className="flex items-center">
{description && <InfoPopup content={description}/>}
{resetValue !== undefined &&
<ResetToDefaultButton fieldName={field.name} defaultValue={resetValue}/>}
</span>
) : undefined
}
isInvalid={!!meta.error}
errorMessage={meta.initialError || meta.error}
disallowEmptySelection
@@ -26,5 +43,3 @@ const SelectInput = ({label, values, ...props}) => {
</div>
);
}
export default SelectInput;
@@ -1,23 +1,41 @@
import {useField} from "formik";
import {Slider as HeroUiSlider} from "@heroui/react";
import {Slider as HeroUiSlider, SliderProps} from "@heroui/react";
import InfoPopup from "Frontend/components/administration/InfoPopup";
import ResetToDefaultButton from "Frontend/components/administration/ResetToDefaultButton";
// @ts-ignore
const SliderInput = ({label, showErrorUntouched = false, ...props}) => {
// @ts-ignore
const [field, meta, helpers] = useField(props);
interface SliderInputProps extends Omit<SliderProps, "name"> {
name: string;
description?: string;
showErrorUntouched?: boolean;
resetValue?: unknown;
}
export default function SliderInput({
label,
showErrorUntouched = false,
description,
resetValue,
...props
}: SliderInputProps) {
const [field, , helpers] = useField<number>(props.name);
return (
<HeroUiSlider
className="min-h-20 grow"
{...props}
value={field.value}
onChange={(value) => helpers.setValue(value)}
onChange={(value) => helpers.setValue(value as number)}
onBlur={field.onBlur}
name={field.name}
id={label}
label={label}
id={label as string}
label={
<span className="flex items-center gap-1">
{label}
{description && <InfoPopup content={description}/>}
{resetValue !== undefined &&
<ResetToDefaultButton fieldName={field.name} defaultValue={resetValue}/>}
</span>
}
/>
);
}
export default SliderInput;
@@ -1,10 +1,13 @@
import {useField} from "formik";
import {Textarea} from "@heroui/react";
import {Textarea, TextAreaProps} from "@heroui/react";
// @ts-ignore
export default function TextAreaInput({label, showErrorUntouched = false, ...props}) {
// @ts-ignore
const [field, meta] = useField(props);
interface TextAreaInputProps extends Omit<TextAreaProps, "name"> {
name: string;
showErrorUntouched?: boolean;
}
export default function TextAreaInput({label, showErrorUntouched = false, ...props}: TextAreaInputProps) {
const [field, meta] = useField(props.name);
return (
<Textarea
@@ -12,7 +15,7 @@ export default function TextAreaInput({label, showErrorUntouched = false, ...pro
fullWidth={false}
{...props}
{...field}
id={label}
id={label as string}
label={label}
isInvalid={(meta.touched || showErrorUntouched) && !!meta.error}
errorMessage={meta.initialError || meta.error}
@@ -1,5 +1,16 @@
import React, {useState} from "react";
import {addToast, Button, Checkbox, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
import {
addToast,
Alert,
Button,
Checkbox,
Link,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader
} from "@heroui/react";
import {Form, Formik} from "formik";
import {LibraryEndpoint} from "Frontend/generated/endpoints";
import Input from "Frontend/components/general/input/Input";
@@ -9,6 +20,7 @@ import ArrayInputAutocomplete from "Frontend/components/general/input/ArrayInput
import {useSnapshot} from "valtio/react";
import {platformState} from "Frontend/state/PlatformState";
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
import {pluginState} from "Frontend/state/PluginState";
interface LibraryCreationModalProps {
isOpen: boolean;
@@ -22,9 +34,10 @@ export default function LibraryCreationModal({
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
const availablePlatforms = useSnapshot(platformState).available;
const hasActiveMetadataPlugins = useSnapshot(pluginState).hasActiveMetadataPlugins;
async function createLibrary(library: LibraryAdminDto) {
await LibraryEndpoint.createLibrary(library, scanAfterCreation);
await LibraryEndpoint.createLibrary(library, hasActiveMetadataPlugins && scanAfterCreation);
addToast({
title: "New library created",
@@ -77,10 +90,21 @@ export default function LibraryCreationModal({
/>
<DirectoryMappingInput name="directories"/>
</div>
{!hasActiveMetadataPlugins &&
<Alert color="warning">
<p>No metadata plugins are currently enabled.</p>
<p>Go to <Link underline="always" color="foreground"
href="/administration/plugins">Plugins</Link> and enable
at least one metadata plugin in order to scan your library.</p>
</Alert>
}
</ModalBody>
<ModalFooter className="flex flex-row justify-between">
<Checkbox isSelected={scanAfterCreation} onValueChange={setScanAfterCreation}>Scan
after creation?</Checkbox>
<Checkbox
isSelected={hasActiveMetadataPlugins && scanAfterCreation}
isDisabled={!hasActiveMetadataPlugins}
onValueChange={setScanAfterCreation}
>Scan after creation?</Checkbox>
<div className="flex flex-row">
<Button variant="light" onPress={onClose}>
Cancel
@@ -6,7 +6,7 @@ import Markdown from "react-markdown";
import remarkBreaks from "remark-breaks";
import {PluginEndpoint} from "Frontend/generated/endpoints";
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
import { ArrowClockwiseIcon } from "@phosphor-icons/react";
import {ArrowClockwiseIcon} from "@phosphor-icons/react";
import PluginConfigMetadataDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginConfigMetadataDto";
import PluginConfigFormField from "Frontend/components/general/plugin/PluginConfigFormField";