Dark mode 2 #100

Merged
rishighan merged 35 commits from dark-mode-2 into main 2024-02-06 10:58:56 +00:00
5 changed files with 252 additions and 257 deletions
Showing only changes of commit 0ade3f9354 - Show all commits

View File

@@ -1,157 +1,13 @@
import { filter, isEmpty, isNil, isUndefined } from "lodash";
import React, { ReactElement, useCallback, useState } from "react";
import Select, { components } from "react-select";
import { fetchComicVineMatches } from "../../../actions/fileops.actions";
import { refineQuery } from "filename-parser";
import { COMICVINE_SERVICE_URI } from "../../../constants/endpoints";
import axios from "axios";
import React, { ReactElement } from "react";
import Select from "react-select";
export const Menu = (props): ReactElement => {
const { data } = props;
const { setSlidingPanelContentId, setVisible } = props.handlers;
const [comicVineMatches, setComicVineMatches] = useState([]);
const fetchComicVineMatches = async (
searchPayload,
issueSearchQuery,
seriesSearchQuery,
) => {
try {
await axios
.request({
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.rawFileDetails,
},
transformResponse: (r) => {
const matches = JSON.parse(r);
return matches;
// return sortBy(matches, (match) => -match.score);
},
})
.then((response) => {
let matches: any = [];
if (
!isNil(response.data.results) &&
response.data.results.length === 1
) {
matches = response.data.results;
} else {
matches = response.data.map((match) => match);
}
setComicVineMatches(matches);
});
} catch (err) {
console.log(err);
}
};
const openDrawerWithCVMatches = () => {
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
if (!isUndefined(data.rawFileDetails)) {
issueSearchQuery = refineQuery(data.rawFileDetails.name);
} else if (!isEmpty(data.sourcedMetadata)) {
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
}
fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery);
setSlidingPanelContentId("CVMatches");
setVisible(true);
};
const openEditMetadataPanel = useCallback(() => {
setSlidingPanelContentId("editComicBookMetadata");
setVisible(true);
}, []);
// Actions menu options and handler
const CVMatchLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--magic-stick-3-bold-duotone] w-6 h-6"></i>
</div>
<div>Match on ComicVine</div>
</span>
);
const editLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--pen-2-bold-duotone] w-6 h-6"></i>
</div>
<div>Edit Metadata</div>
</span>
);
const deleteLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--trash-bin-trash-bold-duotone] w-6 h-6"></i>
</div>
<div>Delete Comic</div>
</span>
);
const Placeholder = (props) => {
return <components.Placeholder {...props} />;
};
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(data.rawFileDetails)) {
return item.value !== "match-on-comic-vine";
}
return item;
});
const handleActionSelection = (action) => {
switch (action.value) {
case "match-on-comic-vine":
openDrawerWithCVMatches();
break;
case "edit-metdata":
openEditMetadataPanel();
break;
default:
console.log("No valid action selected.");
break;
}
};
const customStyles = {
menu: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
}),
placeholder: (base) => ({
...base,
color: "black",
}),
option: (base, { data, isDisabled, isFocused, isSelected }) => ({
...base,
backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)",
}),
singleValue: (base) => ({
...base,
paddingTop: "0.4rem",
}),
control: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
color: "black",
border: "1px solid rgb(156, 163, 175)",
}),
};
const {
filteredActionOptions,
customStyles,
handleActionSelection,
Placeholder,
} = props.configuration;
return (
<Select

View File

@@ -15,7 +15,8 @@ import AcquisitionPanel from "./AcquisitionPanel";
import DownloadsPanel from "./DownloadsPanel";
import { VolumeInformation } from "./Tabs/VolumeInformation";
import { isEmpty, isUndefined, isNil } from "lodash";
import { isEmpty, isUndefined, isNil, filter } from "lodash";
import { components } from "react-select";
import { RootState } from "threetwo-ui-typings";
import "react-sliding-pane/dist/react-sliding-pane.css";
@@ -27,6 +28,9 @@ import ComicViewer from "react-comic-viewer";
import { extractComicArchive } from "../../actions/fileops.actions";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import axios from "axios";
import { COMICVINE_SERVICE_URI } from "../../constants/endpoints";
import { refineQuery } from "filename-parser";
type ComicDetailProps = {};
/**
@@ -56,6 +60,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
const [modalIsOpen, setIsOpen] = useState(false);
const [comicVineMatches, setComicVineMatches] = useState([]);
// const comicVineSearchResults = useSelector(
// (state: RootState) => state.comicInfo.searchResults,
@@ -102,7 +107,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
CVMatches: {
content: (props) => (
<>
{/* <div className="card search-criteria-card">
<div>
<div className="card-content">
<ComicVineSearchForm data={rawFileDetails} />
</div>
@@ -114,29 +119,13 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<span className="tag"> # {inferredMetadata.issue.number} </span>
</div>
) : null}
{!comicVineAPICallProgress ? (
<ComicVineMatchPanel
props={{
comicVineSearchQueryObject,
comicVineAPICallProgress,
comicVineSearchResults,
comicObjectId,
}}
/>
) : (
<div className="progress-indicator-container">
<div className="indicator">
<Loader
type="MutatingDots"
color="#CCC"
secondaryColor="#999"
height={100}
width={100}
visible={comicVineAPICallProgress}
/>
</div>
</div>
)} */}
<ComicVineMatchPanel
props={{
comicVineMatches,
comicObjectId,
}}
/>
</>
),
},
@@ -146,6 +135,154 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
},
};
// Actions
const fetchComicVineMatches = async (
searchPayload,
issueSearchQuery,
seriesSearchQuery,
) => {
try {
await axios
.request({
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.rawFileDetails,
},
transformResponse: (r) => {
const matches = JSON.parse(r);
return matches;
// return sortBy(matches, (match) => -match.score);
},
})
.then((response) => {
console.log(response);
let matches: any = [];
if (
!isNil(response.data.results) &&
response.data.results.length === 1
) {
matches = response.data.results;
} else {
matches = response.data.map((match) => match);
}
setComicVineMatches(matches);
});
} catch (err) {
console.log(err);
}
};
// Action event handlers
const openDrawerWithCVMatches = () => {
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
if (!isUndefined(rawFileDetails)) {
issueSearchQuery = refineQuery(rawFileDetails.name);
} else if (!isEmpty(comicvine)) {
issueSearchQuery = refineQuery(comicvine.name);
}
fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery);
setSlidingPanelContentId("CVMatches");
setVisible(true);
};
const openEditMetadataPanel = useCallback(() => {
setSlidingPanelContentId("editComicBookMetadata");
setVisible(true);
}, []);
// Actions menu options and handler
const CVMatchLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--magic-stick-3-bold-duotone] w-6 h-6"></i>
</div>
<div>Match on ComicVine</div>
</span>
);
const editLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--pen-2-bold-duotone] w-6 h-6"></i>
</div>
<div>Edit Metadata</div>
</span>
);
const deleteLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--trash-bin-trash-bold-duotone] w-6 h-6"></i>
</div>
<div>Delete Comic</div>
</span>
);
const Placeholder = (props) => {
return <components.Placeholder {...props} />;
};
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) => {
switch (action.value) {
case "match-on-comic-vine":
openDrawerWithCVMatches();
break;
case "edit-metdata":
openEditMetadataPanel();
break;
default:
console.log("No valid action selected.");
break;
}
};
const customStyles = {
menu: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
}),
placeholder: (base) => ({
...base,
color: "black",
}),
option: (base, { data, isDisabled, isFocused, isSelected }) => ({
...base,
backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)",
}),
singleValue: (base) => ({
...base,
paddingTop: "0.4rem",
}),
control: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
color: "black",
border: "1px solid rgb(156, 163, 175)",
}),
};
// check for the availability of CV metadata
const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine.volumeInformation);
@@ -285,6 +422,12 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<Menu
data={data.data}
handlers={{ setSlidingPanelContentId, setVisible }}
configuration={{
filteredActionOptions,
customStyles,
handleActionSelection,
Placeholder,
}}
/>
</div>
</RawFileDetails>

View File

@@ -2,22 +2,26 @@ import React, { ReactElement } from "react";
import { ComicVineSearchForm } from "../ComicVineSearchForm";
import MatchResult from "./MatchResult";
import { isEmpty } from "lodash";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
export const ComicVineMatchPanel = (comicVineData): ReactElement => {
const {
comicObjectId,
comicVineSearchQueryObject,
comicVineAPICallProgress,
comicVineSearchResults,
} = comicVineData.props;
const { comicObjectId, comicVineMatches } = comicVineData.props;
const { comicvine } = useStore(
useShallow((state) => ({
comicvine: state.comicvine,
})),
);
return (
<>
<div className="search-results-container">
{!isEmpty(comicVineSearchResults) && (
<div>
{!isEmpty(comicVineMatches) ? (
<MatchResult
matchData={comicVineSearchResults}
matchData={comicVineMatches}
comicObjectId={comicObjectId}
/>
) : (
<>{comicvine.scrapingStatus}</>
)}
</div>
</>

View File

@@ -1,8 +1,9 @@
import React, { useCallback } from "react";
import React from "react";
import { isNil, map } from "lodash";
import { applyComicVineMatch } from "../../actions/comicinfo.actions";
import { convert } from "html-to-text";
import ellipsize from "ellipsize";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import axios from "axios";
interface MatchResultProps {
matchData: any;
@@ -14,12 +15,16 @@ const handleBrokenImage = (e) => {
};
export const MatchResult = (props: MatchResultProps) => {
const applyCVMatch = useCallback(
// (match, comicObjectId) => {
// dispatch(applyComicVineMatch(match, comicObjectId));
// },
[],
);
const applyCVMatch = async (match, comicObjectId) => {
return await axios.request({
url: `${LIBRARY_SERVICE_BASE_URI}/applyComicVineMetadata`,
method: "POST",
data: {
match,
comicObjectId,
},
});
};
return (
<>
{map(props.matchData, (match, idx) => {
@@ -32,45 +37,29 @@ export const MatchResult = (props: MatchResultProps) => {
});
}
return (
<div className="search-result mb-4" key={idx}>
<div className="columns">
<div className="column is-one-fifth">
<div className="mb-4" key={idx}>
<div className="flex flex-row">
<div className="w-full mr-2">
<img
className="cover-image"
className="rounded-md"
src={match.image.thumb_url}
onError={handleBrokenImage}
/>
</div>
<div className="search-result-details column">
<div className="is-size-5">{match.name}</div>
<div className="field is-grouped is-grouped-multiline mt-1">
<div className="control">
<div className="tags has-addons">
<span className="tag">Number</span>
<span className="tag is-primary">
{match.issue_number}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Cover Date</span>
<span className="tag is-warning">{match.cover_date}</span>
</div>
</div>
</div>
<div className="is-size-7">
<div>
<div className="">{match.name}</div>
<span>Number</span>
<span>{match.issue_number}</span>
<span className="tag">Cover Date</span>
<span className="tag is-warning">{match.cover_date}</span>
<div className="text-sm">
{ellipsize(issueDescription, 300)}
</div>
</div>
</div>
<div className="vertical-line"></div>
<div className="columns ml-6 volume-information">
<div className="column is-one-fifth">
<div className="">
<div className="">
<img
src={match.volumeInformation.results.image.icon_url}
className="cover-image"
@@ -78,42 +67,30 @@ export const MatchResult = (props: MatchResultProps) => {
/>
</div>
<div className="column">
<div className="is-size-6">{match.volume.name}</div>
<div className="field is-grouped is-grouped-multiline mt-2">
<div className="control">
<div className="tags has-addons">
<span className="tag">Total Issues</span>
<span className="tag is-warning">
{match.volumeInformation.results.count_of_issues}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Publisher</span>
<span className="tag is-warning">
{match.volumeInformation.results.publisher.name}
</span>
</div>
</div>
<div className="">
<div className="">{match.volume.name}</div>
<div className="">
<span className="">Total Issues</span>
<span className="">
{match.volumeInformation.results.count_of_issues}
</span>
</div>
</div>
<span className="tag">Publisher</span>
<span className="tag is-warning">
{match.volumeInformation.results.publisher.name}
</span>
</div>
<div className="columns">
<div className="column">
<button
className="button is-normal is-outlined is-primary is-light is-pulled-right"
onClick={() => applyCVMatch(match, props.comicObjectId)}
>
<span className="icon is-size-5">
<i className="fas fa-clipboard-check"></i>
</span>
<span>Apply Match</span>
</button>
</div>
</div>
<button
className="button is-normal is-outlined is-primary is-light is-pulled-right"
onClick={() => applyCVMatch(match, props.comicObjectId)}
>
<span className="icon is-size-5">
<i className="fas fa-clipboard-check"></i>
</span>
<span>Apply Match</span>
</button>
</div>
);
})}

View File

@@ -1,5 +1,5 @@
import { create } from "zustand";
import { isEmpty, isNil, isUndefined } from "lodash";
import { isNil } from "lodash";
import io from "socket.io-client";
import { SOCKET_BASE_URI } from "../constants/endpoints";
import { produce } from "immer";
@@ -28,6 +28,11 @@ export const useStore = create((set, get) => ({
// Socket.io state
socketIOInstance: {},
// ComicVine Scraping status
comicvine: {
scrapingStatus: "",
},
// Import job queue and associated statuses
importJobQueue: {
successfulJobCount: 0,
@@ -153,6 +158,16 @@ socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
});
// ComicVine Scraping status
socketIOInstance.on("CV_SCRAPING_STATUS", (data) => {
setState((state) => ({
comicvine: {
...state.comicvine,
scrapingStatus: data.message,
},
}));
});
/**
* Method to init AirDC++ Socket with supplied settings
* @param configuration - credentials, and hostname details to init AirDC++ connection