Compare commits

...

15 Commits

Author SHA1 Message Date
c6f719e78b 💅🏼 Formatting tweaks to tabs 2024-03-30 21:28:48 -04:00
b4d1b678b1 💅🏼 Formatting improvements 2024-03-29 23:26:43 -04:00
aa3192bc1a 🧲 Added visual indicators of torrent progress 2024-03-29 19:35:57 -04:00
56ddfbd16e 🧲 Surfacing torrent progress in UI via scheduled job 2024-03-27 22:23:24 -05:00
173735da45 🧲 Added downloads panel 2024-03-24 17:30:51 -04:00
f4408cd493 🧲 Fixed the auto-population of search box 2024-03-16 18:34:41 -04:00
41a9428729 🧲 Added a torrent download sub-panel 2024-03-07 05:50:49 -06:00
e1da6ddcef 🪢 Wiring up to addTorrent endpoint 2024-02-27 22:15:32 -05:00
df4cecedf0 📝 Added captions to screenshots 2024-02-23 16:59:31 -05:00
3dee53f33f 📝 Updated README 2024-02-23 16:52:45 -05:00
ef05dee600 Removed pngs 2024-02-23 16:42:37 -05:00
2b6bce4731 Merge branch 'main' of https://github.com/rishighan/threetwo 2024-02-23 16:41:21 -05:00
fc4c5c61e2 🌇 Updated screenshots 2024-02-23 16:41:14 -05:00
ae04260f69 Updated .gitignore to ignore storybook static assets (#102)
*  Updated .gitignore to ignore storybook static assets

* 🏗️ Fixed cover-only config for CV detail panel

* 🏗️ Added stats to dashboard

* 🐯 Prowlarr integration WIP

* 🐯 Prowlarr settings form scaffold

* 🧲 Added a form for torrent search

* 🐯 Mocked a prowlarr call
2024-02-23 16:29:13 -05:00
dependabot[bot]
ad5fc0b8b3 Bump ip from 2.0.0 to 2.0.1 (#103)
Bumps [ip](https://github.com/indutny/node-ip) from 2.0.0 to 2.0.1.
- [Commits](https://github.com/indutny/node-ip/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 09:07:19 -06:00
34 changed files with 636 additions and 187 deletions

View File

@@ -6,14 +6,25 @@ ThreeTwo! _aims to be_ a comic book curation app.
### Screenshots ### Screenshots
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/Dashboard.png) #### Dashboard
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/Library.png) ![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/Dashboard.jpg)
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/DC%2B%2B%20integration.png) #### Issue View
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/ComicVine%20Matching.png) ![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/ComicDetail.jpg)
#### DC++ Search
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/DC%2B%2BSearching.jpg)
#### Import
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/Import.jpg)
#### Comic Vine Matching, Metadata Scraping
![](https://raw.githubusercontent.com/rishighan/threetwo/ef05dee6005f683f1e4547631217681def9ebe86/screenshots/CVMatching.jpg)
### 🦄 Early Development Support Channel ### 🦄 Early Development Support Channel
@@ -28,7 +39,8 @@ ThreeTwo! currently is set up as:
1. The UI, this repo. 1. The UI, this repo.
2. [threetwo-core-service](https://github.com/rishighan/threetwo-core-service) 2. [threetwo-core-service](https://github.com/rishighan/threetwo-core-service)
3. [threetwo-metadata-service](https://github.com/rishighan/threetwo-metadata-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 ## 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 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. 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 ## Troubleshooting
### Docker ### Docker
1. `docker-compose up` is taking a long time 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. 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? 2. What folder do my comics go in?
Your comics go in the `comics` directory at the root of this project. Your comics go in the `comics` directory at the root of this project.
## Contribution Guidelines ## Contribution Guidelines
See [contribution guidelines](https://github.com/rishighan/threetwo/blob/master/contributing.md) See [contribution guidelines](https://github.com/rishighan/threetwo/blob/master/contributing.md)

BIN
screenshots/CVMatching.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

BIN
screenshots/ComicDetail.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

BIN
screenshots/Dashboard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
screenshots/Import.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

BIN
screenshots/Library.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

View File

@@ -278,9 +278,9 @@ export const AcquisitionPanel = (
)} )}
/> />
) : ( ) : (
<div className="column is-three-fifths"> <div className="">
<article className="message is-info"> <article className="">
<div className="message-body is-size-6 is-family-secondary"> <div className="">
AirDC++ is not configured. Please configure it in{" "} AirDC++ is not configured. Please configure it in{" "}
<code>Settings &gt; AirDC++ &gt; Connection</code>. <code>Settings &gt; AirDC++ &gt; Connection</code>.
</div> </div>

View 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>
);
};

View File

@@ -12,6 +12,7 @@ import { Menu } from "./ActionMenu/Menu";
import { ArchiveOperations } from "./Tabs/ArchiveOperations"; import { ArchiveOperations } from "./Tabs/ArchiveOperations";
import { ComicInfoXML } from "./Tabs/ComicInfoXML"; import { ComicInfoXML } from "./Tabs/ComicInfoXML";
import AcquisitionPanel from "./AcquisitionPanel"; import AcquisitionPanel from "./AcquisitionPanel";
import TorrentSearchPanel from "./TorrentSearchPanel";
import DownloadsPanel from "./DownloadsPanel"; import DownloadsPanel from "./DownloadsPanel";
import { VolumeInformation } from "./Tabs/VolumeInformation"; import { VolumeInformation } from "./Tabs/VolumeInformation";
@@ -350,13 +351,18 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
</span> </span>
), ),
name: "Torrent Search", name: "Torrent Search",
content: <>Torrents</>, content: <TorrentSearchPanel comicObjectId={_id} issueName={issueName} />,
shouldShow: true, shouldShow: true,
}, },
{ {
id: 6, id: 6,
name: "Downloads", name: "Downloads",
icon: <>{acquisition?.directconnect?.downloads?.length}</>, icon: (
<>
{acquisition?.directconnect?.downloads?.length +
acquisition?.torrent.length}
</>
),
content: content:
!isNil(data.data) && !isEmpty(data.data) ? ( !isNil(data.data) && !isEmpty(data.data) ? (
<DownloadsPanel key={5} /> <DownloadsPanel key={5} />

View File

@@ -1,7 +1,5 @@
import { isEmpty, isNil, isUndefined } from "lodash"; import React, { ReactElement } from "react";
import React, { ReactElement, useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { getComicBookDetailById } from "../../actions/comicinfo.actions";
import { ComicDetail } from "../ComicDetail/ComicDetail"; import { ComicDetail } from "../ComicDetail/ComicDetail";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";

View File

@@ -16,7 +16,7 @@ export const ComicVineDetails = (props): ReactElement => {
<div className="min-w-fit"> <div className="min-w-fit">
<Card <Card
imageUrl={data.volumeInformation.image.thumb_url} imageUrl={data.volumeInformation.image.thumb_url}
orientation={"vertical-2"} orientation={"cover-only"}
hasDetails={false} hasDetails={false}
// cardContainerStyle={{ maxWidth: 200 }} // cardContainerStyle={{ maxWidth: 200 }}
/> />

View File

@@ -1,12 +1,15 @@
import React, { useEffect, useContext, ReactElement, useState } from "react"; import React, { useEffect, useContext, ReactElement, useState } from "react";
import { RootState } from "threetwo-ui-typings"; import { RootState } from "threetwo-ui-typings";
import { isEmpty, map } from "lodash"; import { isEmpty, map } from "lodash";
import prettyBytes from "pretty-bytes"; import { AirDCPPBundles } from "./AirDCPPBundles";
import dayjs from "dayjs"; import { TorrentDownloads } from "./TorrentDownloads";
import ellipsize from "ellipsize";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import axios from "axios"; 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 { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
@@ -20,12 +23,27 @@ export const DownloadsPanel = (
): ReactElement | null => { ): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicObjectId } = useParams<{ comicObjectId: string }>();
const [bundles, setBundles] = useState([]); const [bundles, setBundles] = useState([]);
const { airDCPPSocketInstance } = useStore( const [infoHashes, setInfoHashes] = useState<string[]>([]);
useShallow((state) => ({ const [torrentDetails, setTorrentDetails] = useState([]);
const [activeTab, setActiveTab] = useState("torrents");
const { airDCPPSocketInstance, socketIOInstance } = useStore(
useShallow((state: any) => ({
airDCPPSocketInstance: state.airDCPPSocketInstance, 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++ // Fetch the downloaded files and currently-downloading file(s) from AirDC++
const { data: comicObject, isSuccess } = useQuery({ const { data: comicObject, isSuccess } = useQuery({
queryKey: ["bundles"], 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(() => { useEffect(() => {
getBundles(comicObject).then((result) => { getBundles(comicObject).then((result) => {
setBundles(result); setBundles(result);
}); });
}, [comicObject]); }, [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 ( return (
<div className="columns is-multiline"> <div className="columns is-multiline">
{!isEmpty(airDCPPSocketInstance) && !isEmpty(bundles) && ( {!isEmpty(airDCPPSocketInstance) &&
<Bundles data={bundles} /> !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> </div>
); );
}; };

View File

@@ -48,7 +48,7 @@ export const RawFileDetails = (props): ReactElement => {
<dd className="mt-1 text-sm text-gray-500 dark:text-slate-900"> <dd className="mt-1 text-sm text-gray-500 dark:text-slate-900">
{/* File extension */} {/* 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="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> <i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
</span> </span>

View File

@@ -1,15 +1,9 @@
import React, { ReactElement, useEffect, useState } from "react"; import React, { ReactElement, useState } from "react";
import { isNil } from "lodash"; import { isNil } from "lodash";
export const TabControls = (props): ReactElement => { export const TabControls = (props): ReactElement => {
// const comicBookDetailData = useSelector(
// (state: RootState) => state.comicInfo.comicBookDetail,
// );
const { filteredTabs, downloadCount } = props; const { filteredTabs, downloadCount } = props;
const [active, setActive] = useState(filteredTabs[0].id); const [active, setActive] = useState(filteredTabs[0].id);
// useEffect(() => {
// setActive(filteredTabs[0].id);
// }, [filteredTabs]);
return ( return (
<> <>
@@ -19,7 +13,11 @@ export const TabControls = (props): ReactElement => {
{filteredTabs.map(({ id, name, icon }) => ( {filteredTabs.map(({ id, name, icon }) => (
<a <a
key={id} 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" aria-current="page"
onClick={() => setActive(id)} onClick={() => setActive(id)}
> >
@@ -28,7 +26,7 @@ export const TabControls = (props): ReactElement => {
{id === 6 && !isNil(downloadCount) ? ( {id === 6 && !isNil(downloadCount) ? (
<span className="inline-flex flex-row"> <span className="inline-flex flex-row">
{/* download count */} {/* 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"> <span className="text-md text-slate-500 dark:text-slate-900">
{icon} {icon}
</span> </span>

View File

@@ -30,6 +30,8 @@ export const ArchiveOperations = (props): ReactElement => {
const [currentImage, setCurrentImage] = useState([]); const [currentImage, setCurrentImage] = useState([]);
const [uncompressedArchive, setUncompressedArchive] = useState([]); const [uncompressedArchive, setUncompressedArchive] = useState([]);
const [imageAnalysisResult, setImageAnalysisResult] = useState({}); const [imageAnalysisResult, setImageAnalysisResult] = useState({});
const [shouldRefetchComicBookData, setShouldRefetchComicBookData] =
useState(false);
const constructImagePaths = (data): Array<string> => { const constructImagePaths = (data): Array<string> => {
return data?.map((path: string) => return data?.map((path: string) =>
escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)), escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)),
@@ -63,6 +65,7 @@ export const ArchiveOperations = (props): ReactElement => {
if (isMounted) { if (isMounted) {
setUncompressedArchive(uncompressedArchive); setUncompressedArchive(uncompressedArchive);
setShouldRefetchComicBookData(true);
} }
}, },
}); });
@@ -122,8 +125,9 @@ export const ArchiveOperations = (props): ReactElement => {
enabled: false, enabled: false,
}); });
if (isSuccess) { if (isSuccess && shouldRefetchComicBookData) {
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] }); queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
setShouldRefetchComicBookData(false);
} }
// sliding panel init // sliding panel init
@@ -171,7 +175,8 @@ export const ArchiveOperations = (props): ReactElement => {
</div> </div>
</article> </article>
<div className="mt-5"> <div className="mt-5">
{data.rawFileDetails.archive?.uncompressed ? ( {data.rawFileDetails.archive?.uncompressed &&
!isEmpty(uncompressedArchive) ? (
<article <article
role="alert" 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" 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} ) : null}
<div className="flex flex-row gap-2 mt-4"> <div className="flex flex-row gap-2 mt-4">
{!data.rawFileDetails?.archive?.uncompressed ? ( {isEmpty(uncompressedArchive) ? (
<button <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" 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()} onClick={() => refetch()}

View 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;

View 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;

View File

@@ -5,7 +5,6 @@ import { WantedComicsList } from "./WantedComicsList";
import { VolumeGroups } from "./VolumeGroups"; import { VolumeGroups } from "./VolumeGroups";
import { LibraryStatistics } from "./LibraryStatistics"; import { LibraryStatistics } from "./LibraryStatistics";
import { PullList } from "./PullList"; import { PullList } from "./PullList";
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
@@ -54,16 +53,23 @@ export const Dashboard = (): ReactElement => {
queryKey: ["volumeGroups"], queryKey: ["volumeGroups"],
}); });
// const { data: statistics } = useQuery({
// const libraryStatistics = useSelector( queryFn: async () =>
// (state: RootState) => state.comicInfo.libraryStatistics, await axios({
// ); url: `${LIBRARY_SERVICE_BASE_URI}/libraryStatistics`,
method: "GET",
}),
queryKey: ["libraryStatistics"],
});
return ( return (
<div className="container mx-auto max-w-full"> <div className="container mx-auto max-w-full">
<PullList /> <PullList />
{recentComics && <RecentlyImported comics={recentComics?.data.docs} />} {recentComics && <RecentlyImported comics={recentComics?.data.docs} />}
{/* Wanted comics */} {/* Wanted comics */}
<WantedComicsList comics={wantedComics?.data?.docs} /> <WantedComicsList comics={wantedComics?.data?.docs} />
{/* Library Statistics */}
{statistics && <LibraryStatistics stats={statistics?.data} />}
{/* Volume groups */} {/* Volume groups */}
<VolumeGroups volumeGroups={volumeGroups?.data} /> <VolumeGroups volumeGroups={volumeGroups?.data} />
</div> </div>

View File

@@ -1,113 +1,99 @@
import React, { ReactElement, useEffect } from "react"; import React, { ReactElement, useEffect } from "react";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { isEmpty, isUndefined, map } from "lodash"; import { isEmpty, isUndefined, map } from "lodash";
import Header from "../shared/Header";
export const LibraryStatistics = ( export const LibraryStatistics = (
props: ILibraryStatisticsProps, props: ILibraryStatisticsProps,
): ReactElement => { ): ReactElement => {
// const { stats } = props; const { stats } = props;
return ( return (
<div className="mt-5"> <div className="mt-5">
<h4 className="title is-4"> <Header
<i className="fa-solid fa-chart-simple"></i> Your Library In Numbers headerContent="Your Library In Numbers"
</h4> subHeaderContent={
<p className="subtitle is-7">A brief snapshot of your library.</p> <span className="text-md">A brief snapshot of your library.</span>
<div className="columns is-multiline"> }
<div className="column is-narrow is-two-quarter"> iconClassNames="fa-solid fa-binoculars mr-2"
<dl className="box"> />
<dd className="is-size-4">
<span className="has-text-weight-bold"> <div className="mt-3">
{props.stats.totalDocuments} <div className="flex flex-row gap-5">
</span>{" "} <div className="flex flex-col rounded-lg bg-green-100 dark:bg-green-200 px-4 py-6 text-center">
files <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>
<dd className="is-size-4"> <dd>
Library size <span className="text-2xl text-green-600">
<span className="has-text-weight-bold">
{" "}
{props.stats.comicDirectorySize && {props.stats.comicDirectorySize &&
prettyBytes(props.stats.comicDirectorySize)} prettyBytes(props.stats.comicDirectorySize)}
</span> </span>
</dd> </dd>
</div>
{/* comicinfo and comicvine tagged issues */}
<div className="flex flex-col gap-4">
{!isUndefined(props.stats.statistics) && {!isUndefined(props.stats.statistics) &&
!isEmpty(props.stats.statistics[0].issues) && ( !isEmpty(props.stats.statistics[0].issues) && (
<dd className="is-size-6"> <div className="flex flex-col h-fit rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center">
<span className="has-text-weight-bold"> <span className="text-xl">
{props.stats.statistics[0].issues.length} {props.stats.statistics[0].issues.length}
</span>{" "} </span>{" "}
tagged with ComicVine tagged with ComicVine
</dd> </div>
)} )}
{!isUndefined(props.stats.statistics) && {!isUndefined(props.stats.statistics) &&
!isEmpty(props.stats.statistics[0].issuesWithComicInfoXML) && ( !isEmpty(props.stats.statistics[0].issuesWithComicInfoXML) && (
<dd className="is-size-6"> <div className="flex flex-col h-fit rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center">
<span className="has-text-weight-bold"> <span className="text-xl">
{props.stats.statistics[0].issuesWithComicInfoXML.length} {props.stats.statistics[0].issuesWithComicInfoXML.length}
</span>{" "} </span>{" "}
with
<span className="tag is-warning has-text-weight-bold mr-2 ml-1"> <span className="tag is-warning has-text-weight-bold mr-2 ml-1">
ComicInfo.xml with ComicInfo.xml
</span> </span>
</dd> </div>
)} )}
</dl> </div>
</div>
<div className="p-3 column is-one-quarter"> <div className="">
<dl className="box"> {!isUndefined(props.stats.statistics) &&
<dd className="is-size-6"> !isEmpty(props.stats.statistics[0].fileTypes) &&
<span className="has-text-weight-bold"></span> Issues map(props.stats.statistics[0].fileTypes, (fileType, idx) => {
</dd> return (
<dd className="is-size-6"> <span
<span className="has-text-weight-bold">304</span> Volumes key={idx}
</dd> 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"
<dd className="is-size-6"> >
{!isUndefined(props.stats.statistics) && {fileType.data.length} {fileType._id}
!isEmpty(props.stats.statistics[0].fileTypes) && </span>
map(props.stats.statistics[0].fileTypes, (fileType, idx) => { );
return ( })}
<span key={idx}> </div>
<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>
{/* file types */} {/* file types */}
<div className="p-3 column is-two-fifths"> <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 */} {/* publisher with most issues */}
<dl className="box">
{!isUndefined(props.stats.statistics) && {!isUndefined(props.stats.statistics) &&
!isEmpty( !isEmpty(
props.stats.statistics[0].publisherWithMostComicsInLibrary[0], props.stats.statistics[0].publisherWithMostComicsInLibrary[0],
) && ( ) && (
<dd className="is-size-6"> <>
<span className="has-text-weight-bold"> <span className="">
{ {
props.stats.statistics[0] props.stats.statistics[0]
.publisherWithMostComicsInLibrary[0]._id .publisherWithMostComicsInLibrary[0]._id
} }
</span> </span>
{" has the most issues "} {" has the most issues "}
<span className="has-text-weight-bold"> <span className="">
{ {
props.stats.statistics[0] props.stats.statistics[0]
.publisherWithMostComicsInLibrary[0].count .publisherWithMostComicsInLibrary[0].count
} }
</span> </span>
</dd> </>
)} )}
<dd className="is-size-6"> </div>
<span className="has-text-weight-bold">304</span> Volumes
</dd>
</dl>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -82,7 +82,17 @@ export const PullList = (): ReactElement => {
<div className="content"> <div className="content">
<Header <Header
headerContent="Discover" 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" iconClassNames="fa-solid fa-binoculars mr-2"
link="/pull-list/all/" link="/pull-list/all/"
/> />
@@ -101,7 +111,10 @@ export const PullList = (): ReactElement => {
/> />
{inputValue && ( {inputValue && (
<div className="text-sm"> <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>
)} )}
</div> </div>

View File

@@ -178,7 +178,7 @@ export const Library = (): ReactElement => {
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i> <i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
</span> </span>
<span className="text-md text-slate-900 dark:text-slate-900"> <span className="text-md text-slate-900 dark:text-slate-900">
Torrent: {info.getValue().torrent.downloads.length} Torrent: {info.getValue().torrent.length}
</span> </span>
</span> </span>
</div> </div>

View File

@@ -1,9 +1,7 @@
import React, { useCallback, ReactElement, useState } from "react"; import React, { useCallback, ReactElement, useState } from "react";
import { isNil, isEmpty } from "lodash"; import { isNil, isEmpty } from "lodash";
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings"; 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 { Form, Field } from "react-final-form";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
@@ -27,7 +25,6 @@ export const Search = ({}: ISearchProps): ReactElement => {
const [comicVineMetadata, setComicVineMetadata] = useState({}); const [comicVineMetadata, setComicVineMetadata] = useState({});
const getCVSearchResults = (searchQuery) => { const getCVSearchResults = (searchQuery) => {
setSearchQuery(searchQuery.search); setSearchQuery(searchQuery.search);
// queryClient.invalidateQueries({ queryKey: ["comicvineSearchResults"] });
}; };
const { const {
@@ -146,6 +143,7 @@ export const Search = ({}: ISearchProps): ReactElement => {
)} )}
/> />
</div> </div>
{isLoading && <>Loading kaka...</>}
{!isNil(comicVineSearchResults?.data.results) && {!isNil(comicVineSearchResults?.data.results) &&
!isEmpty(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"> <div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">

View File

@@ -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;

View File

@@ -16,16 +16,7 @@ export const QbittorrentConnectionForm = (): ReactElement => {
}); });
const hostDetails = data?.data?.bittorrent?.client?.host; const hostDetails = data?.data?.bittorrent?.client?.host;
// connect to qbittorrent client // 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 // get qbittorrent client info
const { data: qbittorrentClientInfo } = useQuery({ const { data: qbittorrentClientInfo } = useQuery({
queryKey: ["qbittorrentClientInfo"], queryKey: ["qbittorrentClientInfo"],
@@ -34,7 +25,6 @@ export const QbittorrentConnectionForm = (): ReactElement => {
url: "http://localhost:3060/api/qbittorrent/getClientInfo", url: "http://localhost:3060/api/qbittorrent/getClientInfo",
method: "GET", method: "GET",
}), }),
enabled: !!connectionDetails,
}); });
// Update action using a mutation // Update action using a mutation
const { mutate } = useMutation({ const { mutate } = useMutation({

View File

@@ -3,6 +3,7 @@ import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm"; import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm"; import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm";
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm"; import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
import ProwlarrSettingsForm from "./ProwlarrSettings/ProwlarrSettingsForm";
import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses"; import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
import settingsObject from "../../constants/settings/settingsMenu.json"; import settingsObject from "../../constants/settings/settingsMenu.json";
import { isUndefined, map } from "lodash"; import { isUndefined, map } from "lodash";
@@ -37,6 +38,14 @@ export const Settings = (props: ISettingsProps): ReactElement => {
</div> </div>
), ),
}, },
{
id: "prwlr-connection",
content: (
<>
<ProwlarrSettingsForm />
</>
),
},
{ {
id: "core-service", id: "core-service",
content: <>a</>, content: <>a</>,

View File

@@ -10,6 +10,7 @@ export const WantedComics = (props): ReactElement => {
const { const {
data: wantedComics, data: wantedComics,
isSuccess, isSuccess,
isFetched,
isError, isError,
isLoading, isLoading,
} = useQuery({ } = useQuery({
@@ -41,6 +42,7 @@ export const WantedComics = (props): ReactElement => {
minWidth: 350, minWidth: 350,
accessorFn: (data) => data, accessorFn: (data) => data,
cell: (value) => { cell: (value) => {
console.log("ASDASd", value);
const row = value.getValue()._source; const row = value.getValue()._source;
return row && <MetadataPanel data={row} />; return row && <MetadataPanel data={row} />;
}, },
@@ -172,7 +174,7 @@ export const WantedComics = (props): ReactElement => {
</div> </div>
</div> </div>
</header> </header>
{isSuccess ? ( {isSuccess && wantedComics?.data.hits?.hits ? (
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table

View File

@@ -6,7 +6,6 @@ import { DayPicker, SelectSingleEventHandler } from "react-day-picker";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
export const DatePickerDialog = (props) => { export const DatePickerDialog = (props) => {
console.log(props);
const { setter, apiAction } = props; const { setter, apiAction } = props;
const [selected, setSelected] = useState<Date>(); const [selected, setSelected] = useState<Date>();
const [isPopperOpen, setIsPopperOpen] = useState(false); const [isPopperOpen, setIsPopperOpen] = useState(false);

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
type IHeaderProps = { type IHeaderProps = {
headerContent: string; headerContent: string;
subHeaderContent: string; subHeaderContent: ReactElement;
iconClassNames: string; iconClassNames: string;
link?: string; link?: string;
}; };

View File

@@ -16,7 +16,6 @@ interface IMetadatPanelProps {
containerStyle: any; containerStyle: any;
} }
export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => { export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
console.log(props);
const { const {
rawFileDetails, rawFileDetails,
inferredMetadata, inferredMetadata,

View File

@@ -90,3 +90,17 @@ export const QBITTORRENT_SERVICE_BASE_URI = hostURIBuilder({
port: "3060", port: "3060",
apiPath: `/api/qbittorrent`, 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`,
});

View File

@@ -57,7 +57,7 @@
"displayName": "Prowlarr", "displayName": "Prowlarr",
"children": [ "children": [
{ {
"id": "prowlarr-connection", "id": "prwlr-connection",
"displayName": "Connection" "displayName": "Connection"
}, },
{ {

View File

@@ -8,7 +8,6 @@ import { ErrorPage } from "./components/shared/ErrorPage";
const rootEl = document.getElementById("root"); const rootEl = document.getElementById("root");
const root = createRoot(rootEl); const root = createRoot(rootEl);
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import Import from "./components/Import/Import"; import Import from "./components/Import/Import";
import Dashboard from "./components/Dashboard/Dashboard"; import Dashboard from "./components/Dashboard/Dashboard";
import Search from "./components/Search/Search"; import Search from "./components/Search/Search";
@@ -47,6 +46,5 @@ const router = createBrowserRouter([
root.render( root.render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <RouterProvider router={router} />
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>, </QueryClientProvider>,
); );

View File

@@ -6614,9 +6614,9 @@ invariant@^2.2.4:
loose-envify "^1.0.0" loose-envify "^1.0.0"
ip@^2.0.0: ip@^2.0.0:
version "2.0.0" version "2.0.1"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105"
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==
ipaddr.js@1.9.1: ipaddr.js@1.9.1:
version "1.9.1" version "1.9.1"