🔨 Clearning up the VolumeInformation tab

This commit is contained in:
2026-04-02 13:07:13 -04:00
parent 91592019c4
commit d7ab553120
4 changed files with 472 additions and 30 deletions

View File

@@ -130,6 +130,11 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation);
const hasAnyMetadata =
isComicBookMetadataAvailable ||
!isEmpty(comicInfo) ||
!isNil(locg);
const areRawFileDetailsAvailable =
!isUndefined(rawFileDetails) && !isEmpty(rawFileDetails);
@@ -147,16 +152,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
};
// Create tab configuration
const openReconcilePanel = useCallback(() => {
setSlidingPanelContentId("metadataReconciliation");
setVisible(true);
}, []);
const tabGroup = createTabConfig({
data: data.data,
comicInfo,
isComicBookMetadataAvailable,
hasAnyMetadata,
areRawFileDetailsAvailable,
airDCPPQuery,
comicObjectId: _id,
userSettings,
issueName,
acquisition,
onReconcileMetadata: openReconcilePanel,
});
const filteredTabs = tabGroup.filter((tab) => tab.shouldShow);

View File

@@ -1,15 +1,108 @@
import React, { ReactElement } from "react";
import React, { ReactElement, useMemo } from "react";
import { isEmpty, isNil } from "lodash";
import ComicVineDetails from "../ComicVineDetails";
export const VolumeInformation = (props): ReactElement => {
const { data } = props;
interface ComicVineMetadata {
volumeInformation?: Record<string, unknown>;
name?: string;
number?: string;
resource_type?: string;
id?: number;
}
interface SourcedMetadata {
comicvine?: ComicVineMetadata;
locg?: Record<string, unknown>;
comicInfo?: unknown;
metron?: unknown;
gcd?: unknown;
[key: string]: unknown;
}
interface VolumeInformationData {
sourcedMetadata?: SourcedMetadata;
inferredMetadata?: { issue?: unknown };
updatedAt?: string;
}
interface VolumeInformationProps {
data: VolumeInformationData;
onReconcile?: () => void;
}
const SOURCED_METADATA_KEYS = ["comicvine", "locg", "comicInfo", "metron", "gcd"];
const SOURCE_LABELS: Record<string, string> = {
comicvine: "ComicVine",
locg: "League of Comic Geeks",
comicInfo: "ComicInfo.xml",
metron: "Metron",
gcd: "Grand Comics Database",
inferredMetadata: "Local File",
};
const MetadataSourceChips = ({
sources,
onReconcile,
}: {
sources: string[];
onReconcile: () => void;
}): ReactElement => (
<div className="flex flex-row flex-wrap items-center gap-2 mb-5 p-3 rounded-lg bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
<span className="text-xs text-slate-500 dark:text-slate-400 mr-1">
<i className="icon-[solar--database-outline] w-4 h-4 inline-block align-middle mr-1" />
{sources.length} metadata sources detected
</span>
{sources.map((source) => (
<span
key={source}
className="inline-flex items-center gap-1 bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-medium px-2 py-1 rounded-md border border-slate-200 dark:border-slate-600"
>
<i className="icon-[solar--check-circle-outline] w-3 h-3" />
{SOURCE_LABELS[source] ?? source}
</span>
))}
<button
onClick={onReconcile}
className="ml-auto inline-flex items-center gap-1 text-xs font-semibold text-sky-600 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-200 transition-colors cursor-pointer"
>
<i className="icon-[solar--refresh-outline] w-4 h-4" />
Reconcile sources
</button>
</div>
);
export const VolumeInformation = (props: VolumeInformationProps): ReactElement => {
const { data, onReconcile } = props;
const presentSources = useMemo(() => {
const sources = SOURCED_METADATA_KEYS.filter((key) => {
const val = (data?.sourcedMetadata ?? {})[key];
if (isNil(val) || isEmpty(val)) return false;
// locg returns an object even when empty; require at least one non-null value
if (key === "locg") return Object.values(val as Record<string, unknown>).some((v) => !isNil(v) && v !== "");
return true;
});
if (!isNil(data?.inferredMetadata?.issue) && !isEmpty(data.inferredMetadata.issue)) {
sources.push("inferredMetadata");
}
return sources;
}, [data?.sourcedMetadata, data?.inferredMetadata]);
return (
<div key={1}>
<ComicVineDetails
data={data.sourcedMetadata.comicvine}
updatedAt={data.updatedAt}
/>
{presentSources.length > 1 && (
<MetadataSourceChips
sources={presentSources}
onReconcile={onReconcile ?? (() => {})}
/>
)}
{presentSources.length === 1 && data.sourcedMetadata?.comicvine?.volumeInformation && (
<ComicVineDetails
data={data.sourcedMetadata.comicvine}
updatedAt={data.updatedAt}
/>
)}
</div>
);
};

View File

@@ -2,7 +2,6 @@ import React, { lazy } from "react";
import { isNil, isEmpty } from "lodash";
const VolumeInformation = lazy(() => import("./Tabs/VolumeInformation").then(m => ({ default: m.VolumeInformation })));
const ComicInfoXML = lazy(() => import("./Tabs/ComicInfoXML").then(m => ({ default: m.ComicInfoXML })));
const ArchiveOperations = lazy(() => import("./Tabs/ArchiveOperations").then(m => ({ default: m.ArchiveOperations })));
const AcquisitionPanel = lazy(() => import("./AcquisitionPanel"));
const TorrentSearchPanel = lazy(() => import("./TorrentSearchPanel"));
@@ -18,26 +17,26 @@ interface TabConfig {
interface TabConfigParams {
data: any;
comicInfo: any;
isComicBookMetadataAvailable: boolean;
hasAnyMetadata: boolean;
areRawFileDetailsAvailable: boolean;
airDCPPQuery: any;
comicObjectId: string;
userSettings: any;
issueName: string;
acquisition?: any;
onReconcileMetadata?: () => void;
}
export const createTabConfig = ({
data,
comicInfo,
isComicBookMetadataAvailable,
hasAnyMetadata,
areRawFileDetailsAvailable,
airDCPPQuery,
comicObjectId,
userSettings,
issueName,
acquisition,
onReconcileMetadata,
}: TabConfigParams): TabConfig[] => {
return [
{
@@ -46,23 +45,10 @@ export const createTabConfig = ({
icon: (
<i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
),
content: isComicBookMetadataAvailable ? (
<VolumeInformation data={data} key={1} />
content: hasAnyMetadata ? (
<VolumeInformation data={data} onReconcile={onReconcileMetadata} key={1} />
) : null,
shouldShow: isComicBookMetadataAvailable,
},
{
id: 2,
name: "ComicInfo.xml",
icon: (
<i className="h-5 w-5 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300" />
),
content: (
<div key={2}>
{!isNil(comicInfo) && <ComicInfoXML json={comicInfo} />}
</div>
),
shouldShow: !isEmpty(comicInfo),
shouldShow: hasAnyMetadata,
},
{
id: 3,