mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 16:20:03 +00:00
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:
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user