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
|
### Screenshots
|
||||||
|
|
||||||

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

|

|
||||||
|
|
||||||

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

|

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

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

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

|
||||||
|
|
||||||
### 🦄 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
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">
|
<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 > AirDC++ > Connection</code>.
|
<code>Settings > AirDC++ > Connection</code>.
|
||||||
</div>
|
</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 { 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} />
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
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 { 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
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({
|
||||||
|
|||||||
@@ -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</>,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
"displayName": "Prowlarr",
|
"displayName": "Prowlarr",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "prowlarr-connection",
|
"id": "prwlr-connection",
|
||||||
"displayName": "Connection"
|
"displayName": "Connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user