Dark mode 2 (#100)
* 🗂️ Added tab icons and styles * 🪑 Styled download panel table * 🪑 Cleaned up the DC++ search results table * 🪑 Many changes to DC++ downloads table * 🏗️ Wired up search with RQ * 🏗️ Changes to ComicDetail section * 🔧 Fixing table styes * 🏗 Fixed the archive ops panel * 🔧 Tweaked Archive ops further * 🃏 Styling the action menu * 🧩 CV Match panel refactor WIP * 🏗️ Refactored the action menu * 🤼 Cleaning up CV match panel * 🏗️ Refactored the scored matches * 🤼 Revamped CV match panel UX * 🖌️ Styling tweaks to the side panel * 🖌️ Cleaned up the form * 🏗️ Refactored the search form * 🤐 Added a uncompress indicator * 🏗️ Fix for encoding # in URIs * 🔧 Fixed # symbol handling in URLs * 🔧 Started work on Edit Metadata panel * 🔎 Added a check for existing uncompressed archives * 🏗️ Settings styling tweaks * 🏗️ Fixed invalidation of archiveOps * 🏗️ Fixed an invalidation query on DC++ download panel * 🏗️ Fixed CV-sourced Volume info panel * 🏗️ Fixed volume group card stacks on Dashboard * 🔍 Fixing CV search page * 🏗️ Refactoring Volume groups and wanted panel * 🏗️ Cleaning up useless files * 🛝 Added keen-slider for pull list * 🏗️ Abstracted heading/subheading into Header * 🏗️ Continued refactoring of PullList, Volumes etc. * 📆 Wired up the datepicker to LoCG pull list
This commit was merged in pull request #100.
This commit is contained in:
@@ -1,42 +1,130 @@
|
||||
import React, { ReactElement, useCallback, useState } from "react";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from "react";
|
||||
import { DnD } from "../../shared/Draggable/DnD";
|
||||
import { isEmpty } from "lodash";
|
||||
import Sticky from "react-stickynode";
|
||||
import SlidingPane from "react-sliding-pane";
|
||||
import { extractComicArchive } from "../../../actions/fileops.actions";
|
||||
import { analyzeImage } from "../../../actions/fileops.actions";
|
||||
import { Canvas } from "../../shared/Canvas";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import {
|
||||
IMAGETRANSFORMATION_SERVICE_BASE_URI,
|
||||
LIBRARY_SERVICE_BASE_URI,
|
||||
LIBRARY_SERVICE_HOST,
|
||||
} from "../../../constants/endpoints";
|
||||
import { useStore } from "../../../store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { escapePoundSymbol } from "../../../shared/utils/formatting.utils";
|
||||
|
||||
export const ArchiveOperations = (props): ReactElement => {
|
||||
const { data } = props;
|
||||
// const isComicBookExtractionInProgress = useSelector(
|
||||
// (state: RootState) => state.fileOps.comicBookExtractionInProgress,
|
||||
// );
|
||||
// const extractedComicBookArchive = useSelector(
|
||||
// (state: RootState) => state.fileOps.extractedComicBookArchive.analysis,
|
||||
// );
|
||||
//
|
||||
// const imageAnalysisResult = useSelector((state: RootState) => {
|
||||
// return state.fileOps.imageAnalysisResults;
|
||||
// });
|
||||
|
||||
const unpackComicArchive = useCallback(() => {
|
||||
// dispatch(
|
||||
// extractComicArchive(data.rawFileDetails.filePath, {
|
||||
// type: "full",
|
||||
// purpose: "analysis",
|
||||
// imageResizeOptions: {
|
||||
// baseWidth: 275,
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
}, []);
|
||||
|
||||
const { socketIOInstance } = useStore(
|
||||
useShallow((state) => ({
|
||||
socketIOInstance: state.socketIOInstance,
|
||||
})),
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
// sliding panel config
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
|
||||
// current image
|
||||
const [currentImage, setCurrentImage] = useState([]);
|
||||
const [uncompressedArchive, setUncompressedArchive] = useState([]);
|
||||
const [imageAnalysisResult, setImageAnalysisResult] = useState({});
|
||||
const constructImagePaths = (data): Array<string> => {
|
||||
return data?.map((path: string) =>
|
||||
escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)),
|
||||
);
|
||||
};
|
||||
|
||||
// Listen to the uncompression complete event and orchestrate the final payload
|
||||
socketIOInstance.on("LS_UNCOMPRESSION_JOB_COMPLETE", (data) => {
|
||||
setUncompressedArchive(constructImagePaths(data?.uncompressedArchive));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
if (data.rawFileDetails?.archive?.uncompressed) {
|
||||
const fetchUncompressedArchive = async () => {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `${LIBRARY_SERVICE_BASE_URI}/walkFolders`,
|
||||
method: "POST",
|
||||
data: {
|
||||
basePathToWalk: data?.rawFileDetails?.archive?.expandedPath,
|
||||
extensions: [".jpg", ".jpeg", ".png", ".bmp", "gif"],
|
||||
},
|
||||
transformResponse: async (responseData) => {
|
||||
const parsedData = JSON.parse(responseData);
|
||||
const paths = parsedData.map((pathObject) => {
|
||||
return `${pathObject.containedIn}/${pathObject.name}${pathObject.extension}`;
|
||||
});
|
||||
const uncompressedArchive = constructImagePaths(paths);
|
||||
|
||||
if (isMounted) {
|
||||
setUncompressedArchive(uncompressedArchive);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching uncompressed archive:", error);
|
||||
// Handle error if necessary
|
||||
}
|
||||
};
|
||||
fetchUncompressedArchive();
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
isMounted = false;
|
||||
setUncompressedArchive([]);
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
const analyzeImage = async (imageFilePath: string) => {
|
||||
const response = await axios({
|
||||
url: `${IMAGETRANSFORMATION_SERVICE_BASE_URI}/analyze`,
|
||||
method: "POST",
|
||||
data: {
|
||||
imageFilePath,
|
||||
},
|
||||
});
|
||||
setImageAnalysisResult(response?.data);
|
||||
queryClient.invalidateQueries({ queryKey: ["uncompressedArchive"] });
|
||||
};
|
||||
|
||||
const {
|
||||
data: uncompressionResult,
|
||||
refetch,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = useQuery({
|
||||
queryFn: async () =>
|
||||
await axios({
|
||||
method: "POST",
|
||||
url: `http://localhost:3000/api/library/uncompressFullArchive`,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
data: {
|
||||
filePath: data.rawFileDetails.filePath,
|
||||
comicObjectId: data._id,
|
||||
options: {
|
||||
type: "full",
|
||||
purpose: "analysis",
|
||||
imageResizeOptions: {
|
||||
baseWidth: 275,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
queryKey: ["uncompressedArchive"],
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
if (isSuccess) {
|
||||
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
|
||||
}
|
||||
|
||||
// sliding panel init
|
||||
const contentForSlidingPanel = {
|
||||
@@ -44,13 +132,13 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
content: () => {
|
||||
return (
|
||||
<div>
|
||||
<pre className="is-size-7">{currentImage}</pre>
|
||||
<pre className="text-sm">{currentImage}</pre>
|
||||
{!isEmpty(imageAnalysisResult) ? (
|
||||
<pre className="is-size-7 p-2 mt-3">
|
||||
<pre className="p-2 mt-3">
|
||||
<Canvas data={imageAnalysisResult} />
|
||||
</pre>
|
||||
) : null}
|
||||
<pre className="is-size-7 mt-3">
|
||||
<pre className="font-hasklig mt-3 text-sm">
|
||||
{JSON.stringify(imageAnalysisResult.analyzedData, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -62,54 +150,80 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
// sliding panel handlers
|
||||
const openImageAnalysisPanel = useCallback((imageFilePath) => {
|
||||
setSlidingPanelContentId("imageAnalysis");
|
||||
// dispatch(analyzeImage(imageFilePath));
|
||||
analyzeImage(imageFilePath);
|
||||
setCurrentImage(imageFilePath);
|
||||
setVisible(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div key={2}>
|
||||
<button
|
||||
className={
|
||||
isComicBookExtractionInProgress
|
||||
? "button is-loading is-warning"
|
||||
: "button is-warning"
|
||||
}
|
||||
onClick={unpackComicArchive}
|
||||
<article
|
||||
role="alert"
|
||||
className="mt-4 text-md rounded-lg max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa-solid fa-box-open"></i>
|
||||
</span>
|
||||
<span>Unpack comic archive</span>
|
||||
</button>
|
||||
<div className="columns">
|
||||
<div className="mt-5">
|
||||
{!isEmpty(extractedComicBookArchive) ? (
|
||||
<div>
|
||||
<p>You can perform several operations on your comic book archive.</p>
|
||||
<p>
|
||||
Uncompressing, re-organizing the individual pages, then
|
||||
re-compressing to a different format, for example.
|
||||
</p>
|
||||
<p>You can also analyze color histograms of pages.</p>
|
||||
</div>
|
||||
</article>
|
||||
<div className="mt-5">
|
||||
{data.rawFileDetails.archive?.uncompressed ? (
|
||||
<article
|
||||
role="alert"
|
||||
className="mt-4 text-md rounded-lg max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600"
|
||||
>
|
||||
This issue is already uncompressed at:
|
||||
<p>
|
||||
<code className="font-hasklig text-sm">
|
||||
{data.rawFileDetails.archive.expandedPath}
|
||||
</code>
|
||||
<div className="">It has {uncompressedArchive?.length} pages</div>
|
||||
</p>
|
||||
</article>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-row gap-2 mt-4">
|
||||
{!data.rawFileDetails?.archive?.uncompressed ? (
|
||||
<button
|
||||
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<span className="text-md">Unpack Comic Archive</span>
|
||||
<span className="w-6 h-6">
|
||||
<i className="h-6 w-6 icon-[solar--box-bold-duotone]"></i>
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{!isEmpty(uncompressedArchive) ? (
|
||||
<div>
|
||||
<button
|
||||
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<span className="text-md">Convert to .cbz</span>
|
||||
<span className="w-6 h-6">
|
||||
<i className="h-6 w-6 icon-[solar--zip-file-bold-duotone]"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mt-10">
|
||||
{!isEmpty(uncompressedArchive) ? (
|
||||
<DnD
|
||||
data={extractedComicBookArchive}
|
||||
data={uncompressedArchive}
|
||||
onClickHandler={openImageAnalysisPanel}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{!isEmpty(extractedComicBookArchive) ? (
|
||||
<div className="column mt-5">
|
||||
<Sticky enabled={true} top={70} bottomBoundary={3000}>
|
||||
<div className="card">
|
||||
<div className="card-content">
|
||||
<span className="has-text-size-4">
|
||||
{extractedComicBookArchive.length} pages
|
||||
</span>
|
||||
<button className="button is-small is-light is-primary is-outlined">
|
||||
<span className="icon is-small">
|
||||
<i className="fa-solid fa-compress"></i>
|
||||
</span>
|
||||
<span>Convert to CBZ</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Sticky>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<SlidingPane
|
||||
isOpen={visible}
|
||||
|
||||
@@ -4,52 +4,59 @@ import React, { ReactElement } from "react";
|
||||
export const ComicInfoXML = (data): ReactElement => {
|
||||
const { json } = data;
|
||||
return (
|
||||
<div className="comicInfo-metadata">
|
||||
<dl className="has-text-size-7">
|
||||
<dd className="has-text-weight-medium">{json.series[0]}</dd>
|
||||
<dd className="mt-2 mb-2">
|
||||
<div className="field is-grouped is-grouped-multiline">
|
||||
<div className="control">
|
||||
<span className="tags has-addons">
|
||||
<span className="tag">Pages</span>
|
||||
<span className="tag is-warning is-light">
|
||||
{json.publisher[0]}
|
||||
<div className="flex md:w-4/5 lg:w-78">
|
||||
<dl className="dark:bg-yellow-600 bg-yellow-200 p-3 rounded-lg">
|
||||
<dt>
|
||||
<p className="text-lg">{json.series[0]}</p>
|
||||
</dt>
|
||||
<dd className="text-sm">
|
||||
published by{" "}
|
||||
<span className="underline">
|
||||
{json.publisher[0]}
|
||||
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
|
||||
</span>
|
||||
</dd>
|
||||
<span className="inline-flex flex-row gap-2">
|
||||
{/* Issue number */}
|
||||
{!isUndefined(json.number) && (
|
||||
<dd className="my-2">
|
||||
<span className="inline-flex items-center bg-slate-50 text-sm text-slate-800 font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--hashtag-outline] w-4 h-4"></i>
|
||||
</span>
|
||||
<span className="text-slate-900 dark:text-slate-900">
|
||||
{parseInt(json.number[0], 10)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="control">
|
||||
<span className="tags has-addons">
|
||||
<span className="tag">Issue #</span>
|
||||
<span className="tag is-warning is-light">
|
||||
{!isUndefined(json.number) && parseInt(json.number[0], 10)}
|
||||
</span>
|
||||
</dd>
|
||||
)}
|
||||
<dd className="my-2">
|
||||
{/* Genre */}
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-sm font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--sticker-smile-circle-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="control">
|
||||
<span className="tags has-addons">
|
||||
<span className="tag">Pages</span>
|
||||
<span className="tag is-warning is-light">
|
||||
{json.pagecount[0]}
|
||||
</span>
|
||||
|
||||
<span className="text-slate-500 dark:text-slate-900">
|
||||
{json.genre[0]}
|
||||
</span>
|
||||
</div>
|
||||
{!isUndefined(json.genre) && (
|
||||
<div className="control">
|
||||
<span className="tags has-addons">
|
||||
<span className="tag">Genre</span>
|
||||
<span className="tag is-success is-light">
|
||||
{json.genre[0]}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
</dd>
|
||||
</span>
|
||||
|
||||
<dd className="my-1">
|
||||
{/* Summary */}
|
||||
{!isUndefined(json.summary) && (
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{json.summary[0]}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dd>
|
||||
<span className="is-size-7">{json.notes[0]}</span>
|
||||
</dd>
|
||||
<dd className="mt-1 mb-1">
|
||||
{!isUndefined(json.summary) && json.summary[0]}
|
||||
{/* Notes */}
|
||||
<span className="text-sm text-slate-500 dark:text-slate-900">
|
||||
{json.notes[0]}
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import ComicVineDetails from "../ComicVineDetails";
|
||||
import { convert } from "html-to-text";
|
||||
import { isEmpty } from "lodash";
|
||||
|
||||
export const VolumeInformation = (props): ReactElement => {
|
||||
const { data } = props;
|
||||
const createDescriptionMarkup = (html) => {
|
||||
return { __html: html };
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={1}>
|
||||
<div className="columns is-multiline">
|
||||
<ComicVineDetails
|
||||
data={data.sourcedMetadata.comicvine}
|
||||
updatedAt={data.updatedAt}
|
||||
/>
|
||||
<div className="column is-8">
|
||||
{!isEmpty(data.sourcedMetadata.comicvine.description) &&
|
||||
convert(data.sourcedMetadata.comicvine.description, {
|
||||
baseElements: {
|
||||
selectors: ["p"],
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ComicVineDetails
|
||||
data={data.sourcedMetadata.comicvine}
|
||||
updatedAt={data.updatedAt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user