From 4b8d7b590560f436aba1e22c1454fc4a4ff8b18a Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Thu, 26 Feb 2026 14:06:37 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=82=20Refactoring=20ComicDetail=20to=20ma?= =?UTF-8?q?ke=20it...=20readable=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ComicDetail/ComicDetail.tsx | 372 +++--------------- .../ComicDetail/SlidingPanelContent.tsx | 64 +++ .../ComicDetail/actionMenuConfig.tsx | 65 +++ .../components/ComicDetail/tabConfig.tsx | 128 ++++++ .../ComicDetail/useComicVineMatching.ts | 93 +++++ 5 files changed, 403 insertions(+), 319 deletions(-) create mode 100644 src/client/components/ComicDetail/SlidingPanelContent.tsx create mode 100644 src/client/components/ComicDetail/actionMenuConfig.tsx create mode 100644 src/client/components/ComicDetail/tabConfig.tsx create mode 100644 src/client/components/ComicDetail/useComicVineMatching.ts diff --git a/src/client/components/ComicDetail/ComicDetail.tsx b/src/client/components/ComicDetail/ComicDetail.tsx index 5aa7ae1..78af14c 100644 --- a/src/client/components/ComicDetail/ComicDetail.tsx +++ b/src/client/components/ComicDetail/ComicDetail.tsx @@ -1,34 +1,23 @@ import React, { useState, ReactElement, useCallback } from "react"; import { useParams } from "react-router-dom"; import Card from "../shared/Carda"; -import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; - import { RawFileDetails } from "./RawFileDetails"; -import { ComicVineSearchForm } from "./ComicVineSearchForm"; - import TabControls from "./TabControls"; -import { EditMetadataPanel } from "./EditMetadataPanel"; import { Menu } from "./ActionMenu/Menu"; -import { ArchiveOperations } from "./Tabs/ArchiveOperations"; -import { ComicInfoXML } from "./Tabs/ComicInfoXML"; -import AcquisitionPanel from "./AcquisitionPanel"; -import TorrentSearchPanel from "./TorrentSearchPanel"; -import DownloadsPanel from "./DownloadsPanel"; -import { VolumeInformation } from "./Tabs/VolumeInformation"; - import { isEmpty, isUndefined, isNil, filter } from "lodash"; -import { components, PlaceholderProps, GroupBase, StylesConfig } from "react-select"; - +import { components } from "react-select"; import "react-sliding-pane/dist/react-sliding-pane.css"; import SlidingPane from "react-sliding-pane"; - import { determineCoverFile } from "../../shared/utils/metadata.utils"; -import axios from "axios"; import { styled } from "styled-components"; -import { COMICVINE_SERVICE_URI } from "../../constants/endpoints"; -import { refineQuery } from "filename-parser"; -// overridden with some styles - moved outside component to prevent recreation +// Extracted modules +import { useComicVineMatching } from "./useComicVineMatching"; +import { createTabConfig } from "./tabConfig"; +import { actionOptions, customStyles, ActionOption } from "./actionMenuConfig"; +import { CVMatchesPanel, EditMetadataPanelWrapper } from "./SlidingPanelContent"; + +// Styled component - moved outside to prevent recreation const StyledSlidingPanel = styled(SlidingPane)` background: #ccc; `; @@ -89,29 +78,6 @@ interface ComicDetailProps { comicObjectId?: string; } -interface ComicVineSearchQuery { - inferredIssueDetails: { - name: string; - [key: string]: any; - }; - [key: string]: any; -} - -interface ComicVineMatch { - score: number; - [key: string]: any; -} - -interface ActionOption { - value: string; - label: React.ReactElement; -} - -interface ContentForSlidingPanel { - [key: string]: { - content: (props?: any) => React.ReactElement; - }; -} /** * Component for displaying the metadata for a comic in greater detail. * @@ -121,7 +87,6 @@ interface ContentForSlidingPanel { * * ) */ - export const ComicDetail = (data: ComicDetailProps): ReactElement => { const { data: { @@ -137,32 +102,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { queryClient, comicObjectId: comicObjectIdProp, } = data; + const [activeTab, setActiveTab] = useState(undefined); const [visible, setVisible] = useState(false); const [slidingPanelContentId, setSlidingPanelContentId] = useState(""); const [modalIsOpen, setIsOpen] = useState(false); - const [comicVineMatches, setComicVineMatches] = useState([]); const { comicObjectId } = useParams<{ comicObjectId: string }>(); + const { comicVineMatches, prepareAndFetchMatches } = useComicVineMatching(); - // const dispatch = useDispatch(); - + // Modal handlers (currently unused but kept for future use) const openModal = useCallback((filePath: string) => { setIsOpen(true); - // dispatch( - // extractComicArchive(filePath, { - // type: "full", - // purpose: "reading", - // imageResizeOptions: { - // baseWidth: 1024, - // }, - // }), - // ); }, []); const afterOpenModal = useCallback((things: any) => { - // references are now sync'd and can be accessed. - // subtitle.style.color = "#f00"; console.log("kolaveri", things); }, []); @@ -170,99 +124,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { setIsOpen(false); }, []); - // sliding panel init - const contentForSlidingPanel: ContentForSlidingPanel = { - CVMatches: { - content: (props?: any) => ( - <> -
- -
- -
-

Searching for:

- {inferredMetadata.issue ? ( - <> - {inferredMetadata.issue?.name} - # {inferredMetadata.issue?.number} - - ) : null} -
- { - setVisible(false); - setActiveTab(1); // Switch to Volume Information tab (id: 1) - }, - }} - /> - - ), - }, - - editComicBookMetadata: { - content: () => , - }, - }; - - // Actions - - const fetchComicVineMatches = async ( - searchPayload: any, - issueSearchQuery: ComicVineSearchQuery, - seriesSearchQuery: ComicVineSearchQuery, - ) => { - try { - const response = await axios({ - url: `${COMICVINE_SERVICE_URI}/volumeBasedSearch`, - method: "POST", - data: { - format: "json", - // hack - query: issueSearchQuery.inferredIssueDetails.name - .replace(/[^a-zA-Z0-9 ]/g, "") - .trim(), - limit: "100", - page: 1, - resources: "volume", - scorerConfiguration: { - searchParams: issueSearchQuery.inferredIssueDetails, - }, - rawFileDetails: searchPayload, - }, - transformResponse: (r) => { - const matches = JSON.parse(r); - return matches; - // return sortBy(matches, (match) => -match.score); - }, - }); - let matches: ComicVineMatch[] = []; - if (!isNil(response.data.results) && response.data.results.length === 1) { - matches = response.data.results; - } else { - matches = response.data.map((match: ComicVineMatch) => match); - } - const scoredMatches = matches.sort((a: ComicVineMatch, b: ComicVineMatch) => b.score - a.score); - setComicVineMatches(scoredMatches); - } catch (err) { - console.log(err); - } - }; - // Action event handlers const openDrawerWithCVMatches = () => { - let seriesSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery; - let issueSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery; - - if (!isUndefined(rawFileDetails)) { - issueSearchQuery = refineQuery(rawFileDetails.name) as ComicVineSearchQuery; - } else if (!isEmpty(comicvine) && comicvine?.name) { - issueSearchQuery = refineQuery(comicvine.name) as ComicVineSearchQuery; - } - fetchComicVineMatches(rawFileDetails, issueSearchQuery, seriesSearchQuery); + prepareAndFetchMatches(rawFileDetails, comicvine); setSlidingPanelContentId("CVMatches"); setVisible(true); }; @@ -272,44 +136,15 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { setVisible(true); }, []); - // Actions menu options and handler - const CVMatchLabel = ( - -
- -
-
Match on ComicVine
-
- ); - const editLabel = ( - -
- -
-
Edit Metadata
-
- ); - const deleteLabel = ( - -
- -
-
Delete Comic
-
- ); + // Action menu handler const Placeholder = components.Placeholder; - const actionOptions = [ - { value: "match-on-comic-vine", label: CVMatchLabel }, - { value: "edit-metdata", label: editLabel }, - { value: "delete-comic", label: deleteLabel }, - ]; - const filteredActionOptions = filter(actionOptions, (item) => { if (isUndefined(rawFileDetails)) { return item.value !== "match-on-comic-vine"; } return item; }); + const handleActionSelection = (action: ActionOption) => { switch (action.value) { case "match-on-comic-vine": @@ -323,36 +158,11 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { break; } }; - const customStyles: StylesConfig = { - menu: (base: any) => ({ - ...base, - backgroundColor: "rgb(156, 163, 175)", - }), - placeholder: (base: any) => ({ - ...base, - color: "black", - }), - option: (base: any, { isFocused }: any) => ({ - ...base, - backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)", - }), - singleValue: (base: any) => ({ - ...base, - paddingTop: "0.4rem", - }), - control: (base: any) => ({ - ...base, - backgroundColor: "rgb(156, 163, 175)", - color: "black", - border: "1px solid rgb(156, 163, 175)", - }), - }; - // check for the availability of CV metadata + // Check for metadata availability const isComicBookMetadataAvailable = !isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation); - // check for the availability of rawFileDetails const areRawFileDetailsAvailable = !isUndefined(rawFileDetails) && !isEmpty(rawFileDetails); @@ -362,107 +172,51 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { locg, }); - // query for airdc++ + // Query for airdc++ const airDCPPQuery = { issue: { name: issueName, }, }; - // Tab content and header details - const tabGroup = [ - { - id: 1, - name: "Volume Information", - icon: ( - - ), - content: isComicBookMetadataAvailable ? ( - - ) : null, - shouldShow: isComicBookMetadataAvailable, - }, - { - id: 2, - name: "ComicInfo.xml", - icon: ( - - ), - content: ( -
- {!isNil(comicInfo) && } -
- ), - shouldShow: !isEmpty(comicInfo), - }, - { - id: 3, - icon: ( - - ), - name: "Archive Operations", - content: , - shouldShow: areRawFileDetailsAvailable, - }, - { - id: 4, - icon: ( - - ), - name: "DC++ Search", - content: ( - - ), - shouldShow: true, - }, - { - id: 5, - icon: ( - - - - ), - name: "Torrent Search", - content: , - shouldShow: true, - }, - { - id: 6, - name: "Downloads", - icon: ( - <> - {(acquisition?.directconnect?.downloads?.length || 0) + - (acquisition?.torrent?.length || 0)} - - ), - content: - !isNil(data.data) && !isEmpty(data.data) ? ( - - ) : ( -
-
-
- AirDC++ is not configured. Please configure it in{" "} - Settings. -
-
-
- ), - shouldShow: true, - }, - ]; - // filtered Tabs + // Create tab configuration + const tabGroup = createTabConfig({ + data: data.data, + comicInfo, + isComicBookMetadataAvailable, + areRawFileDetailsAvailable, + airDCPPQuery, + comicObjectId: _id, + userSettings, + issueName, + acquisition, + }); + const filteredTabs = tabGroup.filter((tab) => tab.shouldShow); - // Determine which cover image to use: - // 1. from the locally imported or - // 2. from the CV-scraped version + // Sliding panel content mapping + const renderSlidingPanelContent = () => { + switch (slidingPanelContentId) { + case "CVMatches": + return ( + { + setVisible(false); + setActiveTab(1); + }} + /> + ); + case "editComicBookMetadata": + return ; + default: + return null; + } + }; return (
@@ -503,25 +257,6 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { /> - - {/* - - {extractedComicBook && ( - - )} - */} )} @@ -540,8 +275,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { title={"Comic Vine Search Matches"} width={"600px"} > - {slidingPanelContentId !== "" && - contentForSlidingPanel[slidingPanelContentId]?.content()} + {renderSlidingPanelContent()} )} diff --git a/src/client/components/ComicDetail/SlidingPanelContent.tsx b/src/client/components/ComicDetail/SlidingPanelContent.tsx new file mode 100644 index 0000000..7f97ab3 --- /dev/null +++ b/src/client/components/ComicDetail/SlidingPanelContent.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { ComicVineSearchForm } from "./ComicVineSearchForm"; +import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; +import { EditMetadataPanel } from "./EditMetadataPanel"; + +interface InferredIssue { + name?: string; + number?: number; + year?: string; + subtitle?: string; + [key: string]: any; +} + +interface CVMatchesPanelProps { + rawFileDetails: any; + inferredMetadata: { + issue?: InferredIssue; + }; + comicVineMatches: any[]; + comicObjectId: string; + queryClient: any; + onMatchApplied: () => void; +} + +export const CVMatchesPanel: React.FC = ({ + rawFileDetails, + inferredMetadata, + comicVineMatches, + comicObjectId, + queryClient, + onMatchApplied, +}) => ( + <> +
+ +
+ +
+

Searching for:

+ {inferredMetadata.issue ? ( + <> + {inferredMetadata.issue?.name} + # {inferredMetadata.issue?.number} + + ) : null} +
+ + +); + +interface EditMetadataPanelWrapperProps { + rawFileDetails: any; +} + +export const EditMetadataPanelWrapper: React.FC = ({ + rawFileDetails, +}) => ; diff --git a/src/client/components/ComicDetail/actionMenuConfig.tsx b/src/client/components/ComicDetail/actionMenuConfig.tsx new file mode 100644 index 0000000..c4f5203 --- /dev/null +++ b/src/client/components/ComicDetail/actionMenuConfig.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { StylesConfig } from "react-select"; + +export interface ActionOption { + value: string; + label: React.ReactElement; +} + +export const CVMatchLabel = ( + +
+ +
+
Match on ComicVine
+
+); + +export const editLabel = ( + +
+ +
+
Edit Metadata
+
+); + +export const deleteLabel = ( + +
+ +
+
Delete Comic
+
+); + +export const actionOptions: ActionOption[] = [ + { value: "match-on-comic-vine", label: CVMatchLabel }, + { value: "edit-metdata", label: editLabel }, + { value: "delete-comic", label: deleteLabel }, +]; + +export const customStyles: StylesConfig = { + menu: (base: any) => ({ + ...base, + backgroundColor: "rgb(156, 163, 175)", + }), + placeholder: (base: any) => ({ + ...base, + color: "black", + }), + option: (base: any, { isFocused }: any) => ({ + ...base, + backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)", + }), + singleValue: (base: any) => ({ + ...base, + paddingTop: "0.4rem", + }), + control: (base: any) => ({ + ...base, + backgroundColor: "rgb(156, 163, 175)", + color: "black", + border: "1px solid rgb(156, 163, 175)", + }), +}; diff --git a/src/client/components/ComicDetail/tabConfig.tsx b/src/client/components/ComicDetail/tabConfig.tsx new file mode 100644 index 0000000..3837863 --- /dev/null +++ b/src/client/components/ComicDetail/tabConfig.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { isNil, isEmpty } from "lodash"; +import { VolumeInformation } from "./Tabs/VolumeInformation"; +import { ComicInfoXML } from "./Tabs/ComicInfoXML"; +import { ArchiveOperations } from "./Tabs/ArchiveOperations"; +import AcquisitionPanel from "./AcquisitionPanel"; +import TorrentSearchPanel from "./TorrentSearchPanel"; +import DownloadsPanel from "./DownloadsPanel"; + +interface TabConfig { + id: number; + name: string; + icon: React.ReactElement; + content: React.ReactElement | null; + shouldShow: boolean; +} + +interface TabConfigParams { + data: any; + comicInfo: any; + isComicBookMetadataAvailable: boolean; + areRawFileDetailsAvailable: boolean; + airDCPPQuery: any; + comicObjectId: string; + userSettings: any; + issueName: string; + acquisition?: any; +} + +export const createTabConfig = ({ + data, + comicInfo, + isComicBookMetadataAvailable, + areRawFileDetailsAvailable, + airDCPPQuery, + comicObjectId, + userSettings, + issueName, + acquisition, +}: TabConfigParams): TabConfig[] => { + return [ + { + id: 1, + name: "Volume Information", + icon: ( + + ), + content: isComicBookMetadataAvailable ? ( + + ) : null, + shouldShow: isComicBookMetadataAvailable, + }, + { + id: 2, + name: "ComicInfo.xml", + icon: ( + + ), + content: ( +
+ {!isNil(comicInfo) && } +
+ ), + shouldShow: !isEmpty(comicInfo), + }, + { + id: 3, + icon: ( + + ), + name: "Archive Operations", + content: , + shouldShow: areRawFileDetailsAvailable, + }, + { + id: 4, + icon: ( + + ), + name: "DC++ Search", + content: ( + + ), + shouldShow: true, + }, + { + id: 5, + icon: ( + + + + ), + name: "Torrent Search", + content: , + shouldShow: true, + }, + { + id: 6, + name: "Downloads", + icon: ( + <> + {(acquisition?.directconnect?.downloads?.length || 0) + + (acquisition?.torrent?.length || 0)} + + ), + content: + !isNil(data) && !isEmpty(data) ? ( + + ) : ( +
+
+
+ AirDC++ is not configured. Please configure it in{" "} + Settings. +
+
+
+ ), + shouldShow: true, + }, + ]; +}; diff --git a/src/client/components/ComicDetail/useComicVineMatching.ts b/src/client/components/ComicDetail/useComicVineMatching.ts new file mode 100644 index 0000000..996ce1e --- /dev/null +++ b/src/client/components/ComicDetail/useComicVineMatching.ts @@ -0,0 +1,93 @@ +import { useState } from "react"; +import axios from "axios"; +import { isNil, isUndefined, isEmpty } from "lodash"; +import { refineQuery } from "filename-parser"; +import { COMICVINE_SERVICE_URI } from "../../constants/endpoints"; + +interface ComicVineMatch { + score: number; + [key: string]: any; +} + +interface ComicVineSearchQuery { + inferredIssueDetails: { + name: string; + [key: string]: any; + }; + [key: string]: any; +} + +interface RawFileDetails { + name: string; + [key: string]: any; +} + +interface ComicVineMetadata { + name?: string; + [key: string]: any; +} + +export const useComicVineMatching = () => { + const [comicVineMatches, setComicVineMatches] = useState([]); + + const fetchComicVineMatches = async ( + searchPayload: any, + issueSearchQuery: ComicVineSearchQuery, + seriesSearchQuery: ComicVineSearchQuery, + ) => { + try { + const response = await axios({ + url: `${COMICVINE_SERVICE_URI}/volumeBasedSearch`, + method: "POST", + data: { + format: "json", + // hack + query: issueSearchQuery.inferredIssueDetails.name + .replace(/[^a-zA-Z0-9 ]/g, "") + .trim(), + limit: "100", + page: 1, + resources: "volume", + scorerConfiguration: { + searchParams: issueSearchQuery.inferredIssueDetails, + }, + rawFileDetails: searchPayload, + }, + transformResponse: (r) => { + const matches = JSON.parse(r); + return matches; + }, + }); + let matches: ComicVineMatch[] = []; + if (!isNil(response.data.results) && response.data.results.length === 1) { + matches = response.data.results; + } else { + matches = response.data.map((match: ComicVineMatch) => match); + } + const scoredMatches = matches.sort((a: ComicVineMatch, b: ComicVineMatch) => b.score - a.score); + setComicVineMatches(scoredMatches); + } catch (err) { + console.log(err); + } + }; + + const prepareAndFetchMatches = ( + rawFileDetails: RawFileDetails | undefined, + comicvine: ComicVineMetadata | undefined, + ) => { + let seriesSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery; + let issueSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery; + + if (!isUndefined(rawFileDetails)) { + issueSearchQuery = refineQuery(rawFileDetails.name) as ComicVineSearchQuery; + } else if (!isEmpty(comicvine) && comicvine?.name) { + issueSearchQuery = refineQuery(comicvine.name) as ComicVineSearchQuery; + } + fetchComicVineMatches(rawFileDetails, issueSearchQuery, seriesSearchQuery); + }; + + return { + comicVineMatches, + prepareAndFetchMatches, + }; +};