Compare commits
15 Commits
dark-mode-
...
qbittorren
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f719e78b | |||
| b4d1b678b1 | |||
| aa3192bc1a | |||
| 56ddfbd16e | |||
| 173735da45 | |||
| f4408cd493 | |||
| 41a9428729 | |||
| e1da6ddcef | |||
| df4cecedf0 | |||
| 3dee53f33f | |||
| ef05dee600 | |||
| 2b6bce4731 | |||
| fc4c5c61e2 | |||
| ae04260f69 | |||
|
|
ad5fc0b8b3 |
30
README.md
30
README.md
@@ -6,14 +6,25 @@ ThreeTwo! _aims to be_ a comic book curation app.
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||
#### Dashboard
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
#### Issue View
|
||||
|
||||

|
||||

|
||||
|
||||
#### DC++ Search
|
||||
|
||||

|
||||
|
||||
#### Import
|
||||
|
||||

|
||||
|
||||
#### Comic Vine Matching, Metadata Scraping
|
||||
|
||||

|
||||
|
||||
### 🦄 Early Development Support Channel
|
||||
|
||||
@@ -28,7 +39,8 @@ ThreeTwo! currently is set up as:
|
||||
1. The UI, this repo.
|
||||
2. [threetwo-core-service](https://github.com/rishighan/threetwo-core-service)
|
||||
3. [threetwo-metadata-service](https://github.com/rishighan/threetwo-metadata-service)
|
||||
4. [threetwo-ui-typings](https://github.com/rishighan/threetwo-frontend-types) which are the types used across the UI, installable as an `npm` dependency.
|
||||
4. [threetwo-acquisition-service](https://github.com/rishighan/threetwo-acquisition-service)
|
||||
5. [threetwo-ui-typings](https://github.com/rishighan/threetwo-frontend-types) which are the types used across the UI, installable as an `npm` dependency.
|
||||
|
||||
## Docker Instructions
|
||||
|
||||
@@ -43,20 +55,18 @@ For debugging and troubleshooting, you can run this app locally using these step
|
||||
3. This will open `http://localhost:5173` in your default browser
|
||||
4. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker
|
||||
|
||||
1. `docker-compose up` is taking a long time
|
||||
|
||||
|
||||
This is primarily because `threetwo-import-service` pulls `calibre` from the CDN and it has been known to be extremely slow. I can't find a more reliable alternative, so give it some time to finish downloading.
|
||||
|
||||
2. What folder do my comics go in?
|
||||
|
||||
|
||||
Your comics go in the `comics` directory at the root of this project.
|
||||
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
See [contribution guidelines](https://github.com/rishighan/threetwo/blob/master/contributing.md)
|
||||
|
||||
|
||||
BIN
screenshots/CVMatching.jpg
Normal file
BIN
screenshots/CVMatching.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 533 KiB |
BIN
screenshots/ComicDetail.jpg
Normal file
BIN
screenshots/ComicDetail.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 449 KiB |
BIN
screenshots/DC++Searching.jpg
Normal file
BIN
screenshots/DC++Searching.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 506 KiB |
BIN
screenshots/Dashboard.jpg
Normal file
BIN
screenshots/Dashboard.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
screenshots/Import.jpg
Normal file
BIN
screenshots/Import.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 704 KiB |
BIN
screenshots/Library.jpg
Normal file
BIN
screenshots/Library.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 849 KiB |
@@ -278,9 +278,9 @@ export const AcquisitionPanel = (
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="column is-three-fifths">
|
||||
<article className="message is-info">
|
||||
<div className="message-body is-size-6 is-family-secondary">
|
||||
<div className="">
|
||||
<article className="">
|
||||
<div className="">
|
||||
AirDC++ is not configured. Please configure it in{" "}
|
||||
<code>Settings > AirDC++ > Connection</code>.
|
||||
</div>
|
||||
|
||||
51
src/client/components/ComicDetail/AirDCPPBundles.tsx
Normal file
51
src/client/components/ComicDetail/AirDCPPBundles.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import dayjs from "dayjs";
|
||||
import ellipsize from "ellipsize";
|
||||
import { map } from "lodash";
|
||||
|
||||
export const AirDCPPBundles = (props) => {
|
||||
return (
|
||||
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
|
||||
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Filename
|
||||
</th>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Size
|
||||
</th>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Download Time
|
||||
</th>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Bundle ID
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{map(props.data, (bundle) => (
|
||||
<tr key={bundle.id} className="text-sm">
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
<h5>{ellipsize(bundle.name, 58)}</h5>
|
||||
<span className="text-xs">{ellipsize(bundle.target, 88)}</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
{prettyBytes(bundle.size)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
{dayjs
|
||||
.unix(bundle.time_finished)
|
||||
.format("h:mm on ddd, D MMM, YYYY")}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
<span className="tag is-warning">{bundle.id}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
@@ -350,13 +351,18 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
</span>
|
||||
),
|
||||
name: "Torrent Search",
|
||||
content: <>Torrents</>,
|
||||
content: <TorrentSearchPanel comicObjectId={_id} issueName={issueName} />,
|
||||
shouldShow: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Downloads",
|
||||
icon: <>{acquisition?.directconnect?.downloads?.length}</>,
|
||||
icon: (
|
||||
<>
|
||||
{acquisition?.directconnect?.downloads?.length +
|
||||
acquisition?.torrent.length}
|
||||
</>
|
||||
),
|
||||
content:
|
||||
!isNil(data.data) && !isEmpty(data.data) ? (
|
||||
<DownloadsPanel key={5} />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
import React, { ReactElement, useContext, useEffect, useState } from "react";
|
||||
import React, { ReactElement } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { getComicBookDetailById } from "../../actions/comicinfo.actions";
|
||||
import { ComicDetail } from "../ComicDetail/ComicDetail";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||
|
||||
@@ -16,7 +16,7 @@ export const ComicVineDetails = (props): ReactElement => {
|
||||
<div className="min-w-fit">
|
||||
<Card
|
||||
imageUrl={data.volumeInformation.image.thumb_url}
|
||||
orientation={"vertical-2"}
|
||||
orientation={"cover-only"}
|
||||
hasDetails={false}
|
||||
// cardContainerStyle={{ maxWidth: 200 }}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, { useEffect, useContext, ReactElement, useState } from "react";
|
||||
import { RootState } from "threetwo-ui-typings";
|
||||
import { isEmpty, map } from "lodash";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import dayjs from "dayjs";
|
||||
import ellipsize from "ellipsize";
|
||||
import { AirDCPPBundles } from "./AirDCPPBundles";
|
||||
import { TorrentDownloads } from "./TorrentDownloads";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||
import {
|
||||
LIBRARY_SERVICE_BASE_URI,
|
||||
QBITTORRENT_SERVICE_BASE_URI,
|
||||
TORRENT_JOB_SERVICE_BASE_URI,
|
||||
} from "../../constants/endpoints";
|
||||
import { useStore } from "../../store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useParams } from "react-router-dom";
|
||||
@@ -20,12 +23,27 @@ export const DownloadsPanel = (
|
||||
): ReactElement | null => {
|
||||
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
||||
const [bundles, setBundles] = useState([]);
|
||||
const { airDCPPSocketInstance } = useStore(
|
||||
useShallow((state) => ({
|
||||
const [infoHashes, setInfoHashes] = useState<string[]>([]);
|
||||
const [torrentDetails, setTorrentDetails] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState("torrents");
|
||||
const { airDCPPSocketInstance, socketIOInstance } = useStore(
|
||||
useShallow((state: any) => ({
|
||||
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
||||
socketIOInstance: state.socketIOInstance,
|
||||
})),
|
||||
);
|
||||
|
||||
// React to torrent progress data sent over websockets
|
||||
socketIOInstance.on("AS_TORRENT_DATA", (data) => {
|
||||
const torrents = data.torrents
|
||||
.flatMap(({ _id, details }) => {
|
||||
if (_id === comicObjectId) {
|
||||
return details;
|
||||
}
|
||||
})
|
||||
.filter((item) => item !== undefined);
|
||||
setTorrentDetails(torrents);
|
||||
});
|
||||
// Fetch the downloaded files and currently-downloading file(s) from AirDC++
|
||||
const { data: comicObject, isSuccess } = useQuery({
|
||||
queryKey: ["bundles"],
|
||||
@@ -54,65 +72,75 @@ export const DownloadsPanel = (
|
||||
}
|
||||
};
|
||||
|
||||
// Call the scheduled job for fetching torrent data
|
||||
// triggered by the active tab been set to "torrents"
|
||||
const { data: torrentData } = useQuery({
|
||||
queryFn: () =>
|
||||
axios({
|
||||
url: `${TORRENT_JOB_SERVICE_BASE_URI}/getTorrentData`,
|
||||
method: "GET",
|
||||
params: {
|
||||
trigger: activeTab,
|
||||
},
|
||||
}),
|
||||
queryKey: [activeTab],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getBundles(comicObject).then((result) => {
|
||||
setBundles(result);
|
||||
});
|
||||
}, [comicObject]);
|
||||
|
||||
const Bundles = (props) => {
|
||||
return (
|
||||
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
|
||||
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Filename
|
||||
</th>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Size
|
||||
</th>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Download Time
|
||||
</th>
|
||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Bundle ID
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{map(props.data, (bundle) => (
|
||||
<tr key={bundle.id} className="text-sm">
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
<h5>{ellipsize(bundle.name, 58)}</h5>
|
||||
<span className="text-xs">
|
||||
{ellipsize(bundle.target, 88)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
{prettyBytes(bundle.size)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
{dayjs
|
||||
.unix(bundle.time_finished)
|
||||
.format("h:mm on ddd, D MMM, YYYY")}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||
<span className="tag is-warning">{bundle.id}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="columns is-multiline">
|
||||
{!isEmpty(airDCPPSocketInstance) && !isEmpty(bundles) && (
|
||||
<Bundles data={bundles} />
|
||||
)}
|
||||
{!isEmpty(airDCPPSocketInstance) &&
|
||||
!isEmpty(bundles) &&
|
||||
activeTab === "directconnect" && <AirDCPPBundles data={bundles} />}
|
||||
|
||||
<div>
|
||||
<div className="sm:hidden">
|
||||
<label htmlFor="Download Type" className="sr-only">
|
||||
Download Type
|
||||
</label>
|
||||
|
||||
<select id="Tab" className="w-full rounded-md border-gray-200">
|
||||
<option>DC++ Downloads</option>
|
||||
<option>Torrents</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<nav className="flex gap-6" aria-label="Tabs">
|
||||
<a
|
||||
href="#"
|
||||
className={`shrink-0 rounded-lg p-2 text-sm font-medium hover:bg-gray-50 hover:text-gray-700 ${
|
||||
activeTab === "directconnect"
|
||||
? "bg-slate-200 dark:text-slate-200 dark:bg-slate-400 text-slate-800"
|
||||
: "dark:text-slate-400 text-slate-800"
|
||||
}`}
|
||||
aria-current="page"
|
||||
onClick={() => setActiveTab("directconnect")}
|
||||
>
|
||||
DC++ Downloads
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
className={`shrink-0 rounded-lg p-2 text-sm font-medium hover:bg-gray-50 hover:text-gray-700 ${
|
||||
activeTab === "torrents"
|
||||
? "bg-slate-200 text-slate-800"
|
||||
: "dark:text-slate-400 text-slate-800"
|
||||
}`}
|
||||
onClick={() => setActiveTab("torrents")}
|
||||
>
|
||||
Torrents
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === "torrents" && <TorrentDownloads data={torrentDetails} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export const RawFileDetails = (props): ReactElement => {
|
||||
<dd className="mt-1 text-sm text-gray-500 dark:text-slate-900">
|
||||
{/* File extension */}
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<span className="pt-1">
|
||||
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import React, { ReactElement, useState } from "react";
|
||||
import { isNil } from "lodash";
|
||||
|
||||
export const TabControls = (props): ReactElement => {
|
||||
// const comicBookDetailData = useSelector(
|
||||
// (state: RootState) => state.comicInfo.comicBookDetail,
|
||||
// );
|
||||
const { filteredTabs, downloadCount } = props;
|
||||
const [active, setActive] = useState(filteredTabs[0].id);
|
||||
// useEffect(() => {
|
||||
// setActive(filteredTabs[0].id);
|
||||
// }, [filteredTabs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -19,7 +13,11 @@ export const TabControls = (props): ReactElement => {
|
||||
{filteredTabs.map(({ id, name, icon }) => (
|
||||
<a
|
||||
key={id}
|
||||
className="inline-flex shrink-0 items-center gap-2 border-b border-transparent px-1 py-1 text-md font-medium text-gray-500 dark:text-gray-400 hover:border-gray-300 hover:text-gray-700"
|
||||
className={`inline-flex shrink-0 items-center gap-2 px-1 py-1 text-md font-medium text-gray-500 dark:text-gray-400 hover:border-gray-300 hover:border-b hover:dark:text-slate-200 ${
|
||||
active === id
|
||||
? "border-b border-cyan-50 dark:text-slate-200"
|
||||
: "border-b border-transparent"
|
||||
}`}
|
||||
aria-current="page"
|
||||
onClick={() => setActive(id)}
|
||||
>
|
||||
@@ -28,7 +26,7 @@ export const TabControls = (props): ReactElement => {
|
||||
{id === 6 && !isNil(downloadCount) ? (
|
||||
<span className="inline-flex flex-row">
|
||||
{/* download count */}
|
||||
<span className="inline-flex mx-2 items-center bg-slate-200 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="inline-flex mx-2 items-center bg-slate-200 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-orange-400">
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{icon}
|
||||
</span>
|
||||
|
||||
@@ -30,6 +30,8 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
const [currentImage, setCurrentImage] = useState([]);
|
||||
const [uncompressedArchive, setUncompressedArchive] = useState([]);
|
||||
const [imageAnalysisResult, setImageAnalysisResult] = useState({});
|
||||
const [shouldRefetchComicBookData, setShouldRefetchComicBookData] =
|
||||
useState(false);
|
||||
const constructImagePaths = (data): Array<string> => {
|
||||
return data?.map((path: string) =>
|
||||
escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)),
|
||||
@@ -63,6 +65,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
|
||||
if (isMounted) {
|
||||
setUncompressedArchive(uncompressedArchive);
|
||||
setShouldRefetchComicBookData(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -122,8 +125,9 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
if (isSuccess) {
|
||||
if (isSuccess && shouldRefetchComicBookData) {
|
||||
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
|
||||
setShouldRefetchComicBookData(false);
|
||||
}
|
||||
|
||||
// sliding panel init
|
||||
@@ -171,7 +175,8 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
</div>
|
||||
</article>
|
||||
<div className="mt-5">
|
||||
{data.rawFileDetails.archive?.uncompressed ? (
|
||||
{data.rawFileDetails.archive?.uncompressed &&
|
||||
!isEmpty(uncompressedArchive) ? (
|
||||
<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"
|
||||
@@ -187,7 +192,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-row gap-2 mt-4">
|
||||
{!data.rawFileDetails?.archive?.uncompressed ? (
|
||||
{isEmpty(uncompressedArchive) ? (
|
||||
<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()}
|
||||
|
||||
77
src/client/components/ComicDetail/TorrentDownloads.tsx
Normal file
77
src/client/components/ComicDetail/TorrentDownloads.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
export const TorrentDownloads = (props) => {
|
||||
const { data } = props;
|
||||
console.log(Object.values(data));
|
||||
return (
|
||||
<>
|
||||
{data.map(({ torrent }) => {
|
||||
return (
|
||||
<dl className="mt-5 dark:text-slate-200 text-slate-600">
|
||||
<dt className="text-lg">{torrent.name}</dt>
|
||||
<p className="text-sm">{torrent.hash}</p>
|
||||
<p className="text-sm">
|
||||
Added on {dayjs.unix(torrent.added_on).format("ddd, D MMM, YYYY")}
|
||||
</p>
|
||||
|
||||
<p className="flex gap-2 mt-1">
|
||||
{torrent.progress > 0 ? (
|
||||
<>
|
||||
<progress
|
||||
className="w-80 mt-2 [&::-webkit-progress-bar]:rounded-lg [&::-webkit-progress-value]:rounded-lg [&::-webkit-progress-bar]:bg-slate-300 [&::-webkit-progress-value]:bg-green-400 [&::-moz-progress-bar]:bg-green-400 h-2"
|
||||
value={Math.floor(torrent.progress * 100).toString()}
|
||||
max="100"
|
||||
></progress>
|
||||
|
||||
<span>{Math.floor(torrent.progress * 100)}%</span>
|
||||
|
||||
{/* downloaded/left */}
|
||||
<p className="inline-flex items-center bg-slate-200 text-green-800 dark:text-green-900 text-xs font-medium px-2.5 py-1 rounded-md dark:bg-slate-400">
|
||||
<span className="pr-1">
|
||||
<i className="icon-[solar--arrow-to-down-left-outline] h-4 w-4"></i>
|
||||
</span>
|
||||
<span className="text-md">
|
||||
{prettyBytes(torrent.downloaded)}
|
||||
</span>
|
||||
{/* uploaded */}
|
||||
<span className="pr-1 text-orange-800 dark:text-orange-900 ml-2">
|
||||
<i className="icon-[solar--arrow-to-top-left-outline] h-4 w-4"></i>
|
||||
</span>
|
||||
<span className="text-md text-orange-800 dark:text-orange-900">
|
||||
{prettyBytes(torrent.uploaded)}
|
||||
</span>
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
<div className="flex gap-4 mt-2">
|
||||
{/* Peers */}
|
||||
<span className="inline-flex items-center bg-slate-200 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1">
|
||||
<i className="icon-[solar--station-minimalistic-line-duotone] h-5 w-5"></i>
|
||||
</span>
|
||||
<span className="text-md text-slate-900">
|
||||
{torrent.trackers_count}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Size */}
|
||||
<span className="inline-flex items-center bg-slate-200 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--mirror-right-bold-duotone] h-4 w-4"></i>
|
||||
</span>
|
||||
<span className="text-md text-slate-900">
|
||||
{prettyBytes(torrent.total_size)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TorrentDownloads;
|
||||
200
src/client/components/ComicDetail/TorrentSearchPanel.tsx
Normal file
200
src/client/components/ComicDetail/TorrentSearchPanel.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { Form, Field } from "react-final-form";
|
||||
import {
|
||||
PROWLARR_SERVICE_BASE_URI,
|
||||
QBITTORRENT_SERVICE_BASE_URI,
|
||||
} from "../../constants/endpoints";
|
||||
import { isEmpty, isNil } from "lodash";
|
||||
import ellipsize from "ellipsize";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
export const TorrentSearchPanel = (props) => {
|
||||
const { issueName, comicObjectId } = props;
|
||||
// Initialize searchTerm with issueName from props
|
||||
const [searchTerm, setSearchTerm] = useState({ issueName });
|
||||
const [torrentToDownload, setTorrentToDownload] = useState("");
|
||||
|
||||
const { data, isSuccess, isLoading } = useQuery({
|
||||
queryKey: ["searchResults", searchTerm.issueName],
|
||||
queryFn: async () => {
|
||||
return await axios({
|
||||
url: `${PROWLARR_SERVICE_BASE_URI}/search`,
|
||||
method: "POST",
|
||||
data: {
|
||||
port: "9696",
|
||||
apiKey: "c4f42e265fb044dc81f7e88bd41c3367",
|
||||
offset: 0,
|
||||
categories: [7030],
|
||||
query: searchTerm.issueName,
|
||||
host: "localhost",
|
||||
limit: 100,
|
||||
type: "search",
|
||||
indexerIds: [2],
|
||||
},
|
||||
});
|
||||
},
|
||||
enabled: !isNil(searchTerm.issueName) && searchTerm.issueName.trim() !== "", // Make sure searchTerm is not empty
|
||||
});
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (newTorrent) =>
|
||||
axios.post(`${QBITTORRENT_SERVICE_BASE_URI}/addTorrent`, newTorrent),
|
||||
onSuccess: async (data) => {
|
||||
console.log(data);
|
||||
},
|
||||
});
|
||||
const searchIndexer = (values) => {
|
||||
setSearchTerm({ issueName: values.issueName }); // Update searchTerm based on the form submission
|
||||
};
|
||||
const downloadTorrent = (evt) => {
|
||||
const newTorrent = {
|
||||
comicObjectId,
|
||||
torrentToDownload: evt,
|
||||
};
|
||||
mutation.mutate(newTorrent);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="mt-5">
|
||||
<Form
|
||||
onSubmit={searchIndexer}
|
||||
initialValues={searchTerm}
|
||||
render={({ handleSubmit }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Field name="issueName">
|
||||
{({ input, meta }) => (
|
||||
<div className="max-w-fit">
|
||||
<div className="flex flex-row bg-slate-300 dark:bg-slate-400 rounded-l-lg">
|
||||
<div className="w-10 pl-2 pt-1 text-gray-400 dark:text-gray-200">
|
||||
{/* Icon placeholder */}
|
||||
<i className="icon-[solar--magnifer-bold-duotone] h-7 w-7" />
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
type="text"
|
||||
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||
placeholder="Enter a search term"
|
||||
/>
|
||||
<button
|
||||
className="sm:mt-0 min-w-fit rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
type="submit"
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
Search Indexer
|
||||
<div className="h-5 w-5 ml-1">
|
||||
<i className="h-6 w-6 icon-[solar--magnet-bold-duotone]" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<article
|
||||
role="alert"
|
||||
className="mt-4 rounded-lg text-sm 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"
|
||||
>
|
||||
<div>
|
||||
The default search term is an auto-detected title; you may need to
|
||||
change it to get better matches if the auto-detected one doesn't work.
|
||||
</div>
|
||||
</article>
|
||||
{!isEmpty(data?.data) ? (
|
||||
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200 dark:border-gray-500">
|
||||
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-500 text-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="whitespace-nowrap px-2 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Name
|
||||
</th>
|
||||
<th className="whitespace-nowrap py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Indexer
|
||||
</th>
|
||||
|
||||
<th className="whitespace-nowrap py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-gray-500">
|
||||
{data?.data.map((result, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="px-3 py-3 text-gray-700 dark:text-slate-300 text-md">
|
||||
<p>{ellipsize(result.fileName, 90)}</p>
|
||||
{/* Seeders/Leechers */}
|
||||
<div className="flex gap-3 mt-2">
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--archive-up-minimlistic-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{result.seeders} seeders
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--archive-down-minimlistic-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{result.leechers} leechers
|
||||
</span>
|
||||
</span>
|
||||
{/* Size */}
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{prettyBytes(result.size)}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Files */}
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{result.files} files
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-3 text-gray-700 dark:text-slate-300 text-sm">
|
||||
{result.indexer}
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-3 text-gray-700 dark:text-slate-300 text-sm">
|
||||
<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-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
onClick={() => downloadTorrent(result.downloadUrl)}
|
||||
>
|
||||
<span className="text-xs">Download</span>
|
||||
<span className="w-5 h-5">
|
||||
<i className="h-5 w-5 icon-[solar--download-bold-duotone]"></i>
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TorrentSearchPanel;
|
||||
@@ -5,7 +5,6 @@ import { WantedComicsList } from "./WantedComicsList";
|
||||
import { VolumeGroups } from "./VolumeGroups";
|
||||
import { LibraryStatistics } from "./LibraryStatistics";
|
||||
import { PullList } from "./PullList";
|
||||
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||
@@ -54,16 +53,23 @@ export const Dashboard = (): ReactElement => {
|
||||
queryKey: ["volumeGroups"],
|
||||
});
|
||||
|
||||
//
|
||||
// const libraryStatistics = useSelector(
|
||||
// (state: RootState) => state.comicInfo.libraryStatistics,
|
||||
// );
|
||||
const { data: statistics } = useQuery({
|
||||
queryFn: async () =>
|
||||
await axios({
|
||||
url: `${LIBRARY_SERVICE_BASE_URI}/libraryStatistics`,
|
||||
method: "GET",
|
||||
}),
|
||||
queryKey: ["libraryStatistics"],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-full">
|
||||
<PullList />
|
||||
{recentComics && <RecentlyImported comics={recentComics?.data.docs} />}
|
||||
{/* Wanted comics */}
|
||||
<WantedComicsList comics={wantedComics?.data?.docs} />
|
||||
{/* Library Statistics */}
|
||||
{statistics && <LibraryStatistics stats={statistics?.data} />}
|
||||
{/* Volume groups */}
|
||||
<VolumeGroups volumeGroups={volumeGroups?.data} />
|
||||
</div>
|
||||
|
||||
@@ -1,113 +1,99 @@
|
||||
import React, { ReactElement, useEffect } from "react";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { isEmpty, isUndefined, map } from "lodash";
|
||||
import Header from "../shared/Header";
|
||||
|
||||
export const LibraryStatistics = (
|
||||
props: ILibraryStatisticsProps,
|
||||
): ReactElement => {
|
||||
// const { stats } = props;
|
||||
const { stats } = props;
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<h4 className="title is-4">
|
||||
<i className="fa-solid fa-chart-simple"></i> Your Library In Numbers
|
||||
</h4>
|
||||
<p className="subtitle is-7">A brief snapshot of your library.</p>
|
||||
<div className="columns is-multiline">
|
||||
<div className="column is-narrow is-two-quarter">
|
||||
<dl className="box">
|
||||
<dd className="is-size-4">
|
||||
<span className="has-text-weight-bold">
|
||||
{props.stats.totalDocuments}
|
||||
</span>{" "}
|
||||
files
|
||||
<Header
|
||||
headerContent="Your Library In Numbers"
|
||||
subHeaderContent={
|
||||
<span className="text-md">A brief snapshot of your library.</span>
|
||||
}
|
||||
iconClassNames="fa-solid fa-binoculars mr-2"
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="flex flex-row gap-5">
|
||||
<div className="flex flex-col rounded-lg bg-green-100 dark:bg-green-200 px-4 py-6 text-center">
|
||||
<dt className="text-lg font-medium text-gray-500">Library size</dt>
|
||||
<dd className="text-3xl text-green-600 md:text-5xl">
|
||||
{props.stats.totalDocuments} files
|
||||
</dd>
|
||||
<dd className="is-size-4">
|
||||
Library size
|
||||
<span className="has-text-weight-bold">
|
||||
{" "}
|
||||
<dd>
|
||||
<span className="text-2xl text-green-600">
|
||||
{props.stats.comicDirectorySize &&
|
||||
prettyBytes(props.stats.comicDirectorySize)}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
{/* comicinfo and comicvine tagged issues */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{!isUndefined(props.stats.statistics) &&
|
||||
!isEmpty(props.stats.statistics[0].issues) && (
|
||||
<dd className="is-size-6">
|
||||
<span className="has-text-weight-bold">
|
||||
<div className="flex flex-col h-fit rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center">
|
||||
<span className="text-xl">
|
||||
{props.stats.statistics[0].issues.length}
|
||||
</span>{" "}
|
||||
tagged with ComicVine
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{!isUndefined(props.stats.statistics) &&
|
||||
!isEmpty(props.stats.statistics[0].issuesWithComicInfoXML) && (
|
||||
<dd className="is-size-6">
|
||||
<span className="has-text-weight-bold">
|
||||
<div className="flex flex-col h-fit rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center">
|
||||
<span className="text-xl">
|
||||
{props.stats.statistics[0].issuesWithComicInfoXML.length}
|
||||
</span>{" "}
|
||||
with
|
||||
<span className="tag is-warning has-text-weight-bold mr-2 ml-1">
|
||||
ComicInfo.xml
|
||||
with ComicInfo.xml
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 column is-one-quarter">
|
||||
<dl className="box">
|
||||
<dd className="is-size-6">
|
||||
<span className="has-text-weight-bold"></span> Issues
|
||||
</dd>
|
||||
<dd className="is-size-6">
|
||||
<span className="has-text-weight-bold">304</span> Volumes
|
||||
</dd>
|
||||
<dd className="is-size-6">
|
||||
{!isUndefined(props.stats.statistics) &&
|
||||
!isEmpty(props.stats.statistics[0].fileTypes) &&
|
||||
map(props.stats.statistics[0].fileTypes, (fileType, idx) => {
|
||||
return (
|
||||
<span key={idx}>
|
||||
<span className="has-text-weight-bold">
|
||||
{fileType.data.length}
|
||||
</span>
|
||||
<span className="tag is-warning has-text-weight-bold mr-2 ml-1">
|
||||
{fileType._id}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div className="">
|
||||
{!isUndefined(props.stats.statistics) &&
|
||||
!isEmpty(props.stats.statistics[0].fileTypes) &&
|
||||
map(props.stats.statistics[0].fileTypes, (fileType, idx) => {
|
||||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className="flex flex-col mb-4 h-fit text-xl rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center"
|
||||
>
|
||||
{fileType.data.length} {fileType._id}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* file types */}
|
||||
<div className="p-3 column is-two-fifths">
|
||||
{/* publisher with most issues */}
|
||||
<dl className="box">
|
||||
{/* file types */}
|
||||
<div className="flex flex-col h-fit text-lg rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3">
|
||||
{/* publisher with most issues */}
|
||||
{!isUndefined(props.stats.statistics) &&
|
||||
!isEmpty(
|
||||
props.stats.statistics[0].publisherWithMostComicsInLibrary[0],
|
||||
) && (
|
||||
<dd className="is-size-6">
|
||||
<span className="has-text-weight-bold">
|
||||
<>
|
||||
<span className="">
|
||||
{
|
||||
props.stats.statistics[0]
|
||||
.publisherWithMostComicsInLibrary[0]._id
|
||||
}
|
||||
</span>
|
||||
{" has the most issues "}
|
||||
<span className="has-text-weight-bold">
|
||||
<span className="">
|
||||
{
|
||||
props.stats.statistics[0]
|
||||
.publisherWithMostComicsInLibrary[0].count
|
||||
}
|
||||
</span>
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
<dd className="is-size-6">
|
||||
<span className="has-text-weight-bold">304</span> Volumes
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,17 @@ export const PullList = (): ReactElement => {
|
||||
<div className="content">
|
||||
<Header
|
||||
headerContent="Discover"
|
||||
subHeaderContent="Pull List aggregated for the week from League Of Comic Geeks"
|
||||
subHeaderContent={
|
||||
<span className="text-md">
|
||||
Pull List aggregated for the week from{" "}
|
||||
<span className="underline">
|
||||
<a href="https://leagueofcomicgeeks.com/comics/new-comics">
|
||||
League Of Comic Geeks
|
||||
</a>
|
||||
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
iconClassNames="fa-solid fa-binoculars mr-2"
|
||||
link="/pull-list/all/"
|
||||
/>
|
||||
@@ -101,7 +111,10 @@ export const PullList = (): ReactElement => {
|
||||
/>
|
||||
{inputValue && (
|
||||
<div className="text-sm">
|
||||
Showing pull list for <span>{inputValue}</span>
|
||||
Showing pull list for{" "}
|
||||
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
{inputValue}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@ export const Library = (): ReactElement => {
|
||||
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||
Torrent: {info.getValue().torrent.downloads.length}
|
||||
Torrent: {info.getValue().torrent.length}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { useCallback, ReactElement, useState } from "react";
|
||||
import { isNil, isEmpty } from "lodash";
|
||||
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
|
||||
import { importToDB } from "../../actions/fileops.actions";
|
||||
import { comicinfoAPICall } from "../../actions/comicinfo.actions";
|
||||
import { search } from "../../services/api/SearchApi";
|
||||
|
||||
import { Form, Field } from "react-final-form";
|
||||
import Card from "../shared/Carda";
|
||||
import ellipsize from "ellipsize";
|
||||
@@ -27,7 +25,6 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
const [comicVineMetadata, setComicVineMetadata] = useState({});
|
||||
const getCVSearchResults = (searchQuery) => {
|
||||
setSearchQuery(searchQuery.search);
|
||||
// queryClient.invalidateQueries({ queryKey: ["comicvineSearchResults"] });
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -146,6 +143,7 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isLoading && <>Loading kaka...</>}
|
||||
{!isNil(comicVineSearchResults?.data.results) &&
|
||||
!isEmpty(comicVineSearchResults?.data.results) ? (
|
||||
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Form, Field } from "react-final-form";
|
||||
import { PROWLARR_SERVICE_BASE_URI } from "../../../constants/endpoints";
|
||||
import axios from "axios";
|
||||
|
||||
export const ProwlarrSettingsForm = (props) => {
|
||||
const { data } = useQuery({
|
||||
queryFn: async (): any => {
|
||||
return await axios({
|
||||
url: `${PROWLARR_SERVICE_BASE_URI}/getIndexers`,
|
||||
method: "POST",
|
||||
data: {
|
||||
host: "localhost",
|
||||
port: "9696",
|
||||
apiKey: "c4f42e265fb044dc81f7e88bd41c3367",
|
||||
},
|
||||
});
|
||||
},
|
||||
queryKey: ["prowlarrConnectionResult"],
|
||||
});
|
||||
console.log(data);
|
||||
const submitHandler = () => {};
|
||||
const initialData = {};
|
||||
return (
|
||||
<>
|
||||
Prowlarr Settings.
|
||||
<Form
|
||||
onSubmit={submitHandler}
|
||||
initialValues={initialData}
|
||||
render={({ handleSubmit }) => (
|
||||
<form>
|
||||
<article
|
||||
role="alert"
|
||||
className="mt-4 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"
|
||||
>
|
||||
<div>
|
||||
<p>Configure Prowlarr integration here.</p>
|
||||
<p>
|
||||
Note that you need a Prowlarr instance hosted and running to
|
||||
configure the integration.
|
||||
</p>
|
||||
<p>
|
||||
See{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href="http://airdcpp.net/docs/installation/installation.html"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
for Prowlarr installation instructions for various platforms.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProwlarrSettingsForm;
|
||||
@@ -16,16 +16,7 @@ export const QbittorrentConnectionForm = (): ReactElement => {
|
||||
});
|
||||
const hostDetails = data?.data?.bittorrent?.client?.host;
|
||||
// connect to qbittorrent client
|
||||
const { data: connectionDetails } = useQuery({
|
||||
queryKey: [],
|
||||
queryFn: async () =>
|
||||
await axios({
|
||||
url: "http://localhost:3060/api/qbittorrent/connect",
|
||||
method: "POST",
|
||||
data: hostDetails,
|
||||
}),
|
||||
enabled: !!hostDetails,
|
||||
});
|
||||
|
||||
// get qbittorrent client info
|
||||
const { data: qbittorrentClientInfo } = useQuery({
|
||||
queryKey: ["qbittorrentClientInfo"],
|
||||
@@ -34,7 +25,6 @@ export const QbittorrentConnectionForm = (): ReactElement => {
|
||||
url: "http://localhost:3060/api/qbittorrent/getClientInfo",
|
||||
method: "GET",
|
||||
}),
|
||||
enabled: !!connectionDetails,
|
||||
});
|
||||
// Update action using a mutation
|
||||
const { mutate } = useMutation({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
|
||||
import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
|
||||
import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm";
|
||||
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
|
||||
import ProwlarrSettingsForm from "./ProwlarrSettings/ProwlarrSettingsForm";
|
||||
import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
|
||||
import settingsObject from "../../constants/settings/settingsMenu.json";
|
||||
import { isUndefined, map } from "lodash";
|
||||
@@ -37,6 +38,14 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "prwlr-connection",
|
||||
content: (
|
||||
<>
|
||||
<ProwlarrSettingsForm />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "core-service",
|
||||
content: <>a</>,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const WantedComics = (props): ReactElement => {
|
||||
const {
|
||||
data: wantedComics,
|
||||
isSuccess,
|
||||
isFetched,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useQuery({
|
||||
@@ -41,6 +42,7 @@ export const WantedComics = (props): ReactElement => {
|
||||
minWidth: 350,
|
||||
accessorFn: (data) => data,
|
||||
cell: (value) => {
|
||||
console.log("ASDASd", value);
|
||||
const row = value.getValue()._source;
|
||||
return row && <MetadataPanel data={row} />;
|
||||
},
|
||||
@@ -172,7 +174,7 @@ export const WantedComics = (props): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{isSuccess ? (
|
||||
{isSuccess && wantedComics?.data.hits?.hits ? (
|
||||
<div>
|
||||
<div className="library">
|
||||
<T2Table
|
||||
|
||||
@@ -6,7 +6,6 @@ import { DayPicker, SelectSingleEventHandler } from "react-day-picker";
|
||||
import { usePopper } from "react-popper";
|
||||
|
||||
export const DatePickerDialog = (props) => {
|
||||
console.log(props);
|
||||
const { setter, apiAction } = props;
|
||||
const [selected, setSelected] = useState<Date>();
|
||||
const [isPopperOpen, setIsPopperOpen] = useState(false);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
|
||||
|
||||
type IHeaderProps = {
|
||||
headerContent: string;
|
||||
subHeaderContent: string;
|
||||
subHeaderContent: ReactElement;
|
||||
iconClassNames: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ interface IMetadatPanelProps {
|
||||
containerStyle: any;
|
||||
}
|
||||
export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
||||
console.log(props);
|
||||
const {
|
||||
rawFileDetails,
|
||||
inferredMetadata,
|
||||
|
||||
@@ -90,3 +90,17 @@ export const QBITTORRENT_SERVICE_BASE_URI = hostURIBuilder({
|
||||
port: "3060",
|
||||
apiPath: `/api/qbittorrent`,
|
||||
});
|
||||
|
||||
export const PROWLARR_SERVICE_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3060",
|
||||
apiPath: `/api/prowlarr`,
|
||||
});
|
||||
|
||||
export const TORRENT_JOB_SERVICE_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3000",
|
||||
apiPath: `/api/torrentjobs`,
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"displayName": "Prowlarr",
|
||||
"children": [
|
||||
{
|
||||
"id": "prowlarr-connection",
|
||||
"id": "prwlr-connection",
|
||||
"displayName": "Connection"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ErrorPage } from "./components/shared/ErrorPage";
|
||||
const rootEl = document.getElementById("root");
|
||||
const root = createRoot(rootEl);
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import Import from "./components/Import/Import";
|
||||
import Dashboard from "./components/Dashboard/Dashboard";
|
||||
import Search from "./components/Search/Search";
|
||||
@@ -47,6 +46,5 @@ const router = createBrowserRouter([
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<ReactQueryDevtools initialIsOpen={true} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
@@ -6614,9 +6614,9 @@ invariant@^2.2.4:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
ip@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
|
||||
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105"
|
||||
integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
|
||||
Reference in New Issue
Block a user