Implement metadata completeness indicator

Show metadata completeness
This commit is contained in:
Simon
2025-09-03 18:00:21 +02:00
committed by GitHub
4 changed files with 58 additions and 2 deletions
@@ -0,0 +1,27 @@
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
import {useMemo} from "react";
import {CircularProgress} from "@heroui/react";
import {metadataCompleteness} from "Frontend/util/utils";
interface MetadataCompletenessIndicatorProps {
game: GameDto;
}
export default function MetadataCompletenessIndicator({game}: MetadataCompletenessIndicatorProps) {
const completeness = useMemo(() => metadataCompleteness(game), [game]);
const color = useMemo(() => {
return completeness > 80 ? "success" : completeness > 50 ? "warning" : "danger";
}, [completeness]);
return <div className="flex flex-row items-center gap-1">
<CircularProgress
color={color}
value={completeness}
disableAnimation
size="sm"
strokeWidth={5}
/>
<p>{completeness}% </p>
</div>;
}
@@ -26,6 +26,8 @@ import {useMemo, useState} from "react";
import EditGameMetadataModal from "Frontend/components/general/modals/EditGameMetadataModal";
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
import {GameAdminDto} from "Frontend/dtos/GameDtos";
import MetadataCompletenessIndicator from "Frontend/components/general/MetadataCompletenessIndicator";
import {metadataCompleteness} from "Frontend/util/utils";
interface LibraryManagementGamesProps {
library: LibraryDto;
@@ -67,6 +69,9 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
case "downloadCount":
cmp = a.metadata.downloadCount - b.metadata.downloadCount;
break;
case "completeness":
cmp = metadataCompleteness(a) - metadataCompleteness(b);
break;
default:
return 0; // No sorting if the column is not recognized
}
@@ -160,6 +165,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
<TableColumn key="addedToLibrary" allowsSorting>Added to library</TableColumn>
<TableColumn key="downloadCount" allowsSorting>Download count</TableColumn>
<TableColumn>Path</TableColumn>
<TableColumn key="completeness" allowsSorting>Completeness</TableColumn>
{/* width={1} keeps the column as far to the right as possible*/}
<TableColumn width={1}>Actions</TableColumn>
</TableHeader>
@@ -182,6 +188,9 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
<TableCell>
{item.metadata.path}
</TableCell>
<TableCell>
<MetadataCompletenessIndicator game={item}/>
</TableCell>
<TableCell>
<div className="flex flex-row gap-2">
<Button isIconOnly size="sm" onPress={() => toggleMatchConfirmed(item)}>
+20
View File
@@ -1,5 +1,6 @@
import {getCsrfToken} from "Frontend/util/auth";
import moment from 'moment-timezone';
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
export function isAdmin(auth: any): boolean {
return auth.state.user?.roles?.some((a: string) => a?.includes("ADMIN"));
@@ -207,4 +208,23 @@ export function fileNameFromPath(path: string, includeExtension: boolean = true)
}
const dotIndex = fileName.lastIndexOf('.');
return dotIndex < 0 ? fileName : fileName.substring(0, dotIndex);
}
/** Calculate the completeness of a GameDto
* @param game
* @returns completeness percentage (0-100)
*/
export function metadataCompleteness(game: GameDto) {
// Total number of fields considered for completeness
// Includes all fields except "comment"
const totalFields = 21;
const filledFields = Object.values(game).filter(value => {
if (value === null || value === undefined) return false;
if (Array.isArray(value)) return value.length > 0;
if (typeof value === "string") return value.trim().length > 0;
return true;
}).length;
return Math.round((filledFields / totalFields) * 100);
}
@@ -29,7 +29,7 @@ sealed interface GameDto {
val metadata: GameMetadataDto
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.ALWAYS)
data class GameUserDto(
override val id: Long,
override val createdAt: Instant,
@@ -55,7 +55,7 @@ data class GameUserDto(
override val metadata: GameMetadataUserDto
) : GameDto
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.ALWAYS)
data class GameAdminDto(
override val id: Long,
override val createdAt: Instant,