diff --git a/src/client/components/Dashboard/RecentlyImported.tsx b/src/client/components/Dashboard/RecentlyImported.tsx
index cf089aa..ab7fc43 100644
--- a/src/client/components/Dashboard/RecentlyImported.tsx
+++ b/src/client/components/Dashboard/RecentlyImported.tsx
@@ -1,5 +1,5 @@
import React, { ReactElement } from "react";
-import Card from "../Carda";
+import Card from "../shared/Carda";
import { Link } from "react-router-dom";
import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash";
diff --git a/src/client/components/Dashboard/WantedComicsList.tsx b/src/client/components/Dashboard/WantedComicsList.tsx
index 2205b79..9cd64f7 100644
--- a/src/client/components/Dashboard/WantedComicsList.tsx
+++ b/src/client/components/Dashboard/WantedComicsList.tsx
@@ -1,5 +1,5 @@
import React, { ReactElement } from "react";
-import Card from "../Carda";
+import Card from "../shared/Carda";
import { Link, useNavigate } from "react-router-dom";
import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash";
diff --git a/src/client/components/GlobalSearchBar/SearchBar.tsx b/src/client/components/GlobalSearchBar/SearchBar.tsx
index bea5457..b1ee5e0 100644
--- a/src/client/components/GlobalSearchBar/SearchBar.tsx
+++ b/src/client/components/GlobalSearchBar/SearchBar.tsx
@@ -1,7 +1,7 @@
import { debounce, isEmpty, map } from "lodash";
import React, { ReactElement, useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
-import Card from "../Carda";
+import Card from "../shared/Carda";
import { searchIssue } from "../../actions/fileops.actions";
import MetadataPanel from "../shared/MetadataPanel";
diff --git a/src/client/components/Import.tsx b/src/client/components/Import/Import.tsx
similarity index 99%
rename from src/client/components/Import.tsx
rename to src/client/components/Import/Import.tsx
index 0d36270..d961df6 100644
--- a/src/client/components/Import.tsx
+++ b/src/client/components/Import/Import.tsx
@@ -4,7 +4,7 @@ import {
fetchComicBookMetadata,
getImportJobResultStatistics,
setQueueControl,
-} from "../actions/fileops.actions";
+} from "../../actions/fileops.actions";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import { format } from "date-fns";
import Loader from "react-loader-spinner";
diff --git a/src/client/components/Library/LibraryGrid.tsx b/src/client/components/Library/LibraryGrid.tsx
index 4790e55..04c7a85 100644
--- a/src/client/components/Library/LibraryGrid.tsx
+++ b/src/client/components/Library/LibraryGrid.tsx
@@ -12,7 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
import { getComicBooks } from "../../actions/fileops.actions";
import { isNil, isEmpty, isUndefined } from "lodash";
import Masonry from "react-masonry-css";
-import Card from "../Carda";
+import Card from "../shared/Carda";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import { Link } from "react-router-dom";
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";
diff --git a/src/client/components/PullList/PullList.tsx b/src/client/components/PullList/PullList.tsx
index 0090c67..b264248 100644
--- a/src/client/components/PullList/PullList.tsx
+++ b/src/client/components/PullList/PullList.tsx
@@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useMemo } from "react";
import T2Table from "../shared/T2Table";
import { getWeeklyPullList } from "../../actions/comicinfo.actions";
import { useDispatch, useSelector } from "react-redux";
-import Card from "../Carda";
+import Card from "../shared/Carda";
import ellipsize from "ellipsize";
import { isNil } from "lodash";
diff --git a/src/client/components/Search.tsx b/src/client/components/Search/Search.tsx
similarity index 96%
rename from src/client/components/Search.tsx
rename to src/client/components/Search/Search.tsx
index 8682c2d..6001ff9 100644
--- a/src/client/components/Search.tsx
+++ b/src/client/components/Search/Search.tsx
@@ -1,12 +1,12 @@
import React, { useCallback, ReactElement } from "react";
import { isNil, isEmpty } from "lodash";
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
-import { importToDB } from "../actions/fileops.actions";
+import { importToDB } from "../../actions/fileops.actions";
import { useSelector, useDispatch } from "react-redux";
-import { comicinfoAPICall } from "../actions/comicinfo.actions";
-import { search } from "../services/api/SearchApi";
+import { comicinfoAPICall } from "../../actions/comicinfo.actions";
+import { search } from "../../services/api/SearchApi";
import { Form, Field } from "react-final-form";
-import Card from "./Carda";
+import Card from "../shared/Carda";
import ellipsize from "ellipsize";
import { convert } from "html-to-text";
import dayjs from "dayjs";
diff --git a/src/client/components/AirDCPPSettings/AirDCPPHubsForm.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx
similarity index 95%
rename from src/client/components/AirDCPPSettings/AirDCPPHubsForm.tsx
rename to src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx
index 11841fd..c2a88bf 100644
--- a/src/client/components/AirDCPPSettings/AirDCPPHubsForm.tsx
+++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx
@@ -3,8 +3,8 @@ import { Form, Field } from "react-final-form";
import { useDispatch } from "react-redux";
import { isEmpty, isNil, isUndefined } from "lodash";
import Select from "react-select";
-import { saveSettings } from "../../actions/settings.actions";
-import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
+import { saveSettings } from "../../../actions/settings.actions";
+import { AirDCPPSocketContext } from "../../../context/AirDCPPSocket";
export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => {
const dispatch = useDispatch();
diff --git a/src/client/components/AirDCPPSettings/AirDCPPSettingsConfirmation.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsConfirmation.tsx
similarity index 97%
rename from src/client/components/AirDCPPSettings/AirDCPPSettingsConfirmation.tsx
rename to src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsConfirmation.tsx
index a7b5d0a..e5e604e 100644
--- a/src/client/components/AirDCPPSettings/AirDCPPSettingsConfirmation.tsx
+++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsConfirmation.tsx
@@ -2,7 +2,6 @@ import React, { ReactElement } from "react";
export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
const { settings } = settingsObject;
- console.log(settings);
return (
diff --git a/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx
new file mode 100644
index 0000000..4f97895
--- /dev/null
+++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx
@@ -0,0 +1,67 @@
+import React, { ReactElement, useCallback } from "react";
+import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
+import { isUndefined, isEmpty } from "lodash";
+import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
+import { useStore } from "../../../store/index";
+import { useShallow } from "zustand/react/shallow";
+
+export const AirDCPPSettingsForm = (): ReactElement => {
+ // cherry-picking selectors for:
+ // 1. initial values for the form
+ // 2. If initial values are present, get the socket information to display
+ const {
+ airDCPPSocketConnected,
+ airDCPPDisconnectionInfo,
+ airDCPPSocketConnectionInformation,
+ airDCPPClientConfiguration,
+ } = useStore(
+ useShallow((state) => ({
+ airDCPPSocketConnected: state.airDCPPSocketConnected,
+ airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo,
+ airDCPPClientConfiguration: state.airDCPPClientConfiguration,
+ airDCPPSocketConnectionInformation:
+ state.airDCPPSocketConnectionInformation,
+ })),
+ );
+
+ const onSubmit = useCallback(async (values) => {
+ try {
+ // airDCPPSettings.setSettings(values);
+ } catch (error) {
+ console.log(error);
+ }
+ }, []);
+ const removeSettings = useCallback(async () => {
+ // airDCPPSettings.setSettings({});
+ }, []);
+ //
+ const initFormData = !isUndefined(airDCPPClientConfiguration)
+ ? airDCPPClientConfiguration
+ : {};
+
+ return (
+ <>
+
+
+ {!isEmpty(airDCPPSocketConnectionInformation) ? (
+
+ ) : null}
+
+ {!isEmpty(airDCPPClientConfiguration) ? (
+
+
+
+ ) : null}
+ >
+ );
+};
+
+export default AirDCPPSettingsForm;
diff --git a/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx b/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx
new file mode 100644
index 0000000..f0cbfda
--- /dev/null
+++ b/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx
@@ -0,0 +1,72 @@
+import React, { ReactElement } from "react";
+import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import axios from "axios";
+
+export const QbittorrentConnectionForm = (): ReactElement => {
+ // fetch settings
+ const { data, isLoading, isError } = useQuery({
+ queryKey: ["settings"],
+ queryFn: async () =>
+ await axios({
+ url: "http://localhost:3000/api/settings/getAllSettings",
+ method: "GET",
+ }),
+ });
+ const hostDetails = data?.data.bittorrent.client.host;
+ // connect to qbittorrent client
+ const { data: connectionDetails } = useQuery({
+ queryKey: [],
+ queryFn: async () =>
+ await axios({
+ url: "http://localhost:3060/api/qbittorrent/connect",
+ method: "POST",
+ data: hostDetails,
+ }),
+ enabled: !!hostDetails,
+ });
+ // get qbittorrent client info
+ const { data: qbittorrentClientInfo } = useQuery({
+ queryKey: ["qbittorrentClientInfo"],
+ queryFn: async () =>
+ await axios({
+ url: "http://localhost:3060/api/qbittorrent/getClientInfo",
+ method: "GET",
+ }),
+ enabled: !!connectionDetails,
+ });
+ console.log(qbittorrentClientInfo?.data);
+ // Update action using a mutation
+ const { mutate } = useMutation({
+ mutationFn: async (values) =>
+ await axios({
+ url: `http://localhost:3000/api/settings/saveSettings`,
+ method: "POST",
+ data: { settingsPayload: values, settingsKey: "bittorrent" },
+ }),
+ });
+
+ if (isError)
+ return (
+ <>
+
Something went wrong connecting to qBittorrent.
+ >
+ );
+ if (!isLoading) {
+ return (
+ <>
+
+
+
+ {JSON.stringify(qbittorrentClientInfo?.data, null, 4)}
+
+ >
+ );
+ }
+};
+
+export default QbittorrentConnectionForm;
diff --git a/src/client/components/Settings.tsx b/src/client/components/Settings/Settings.tsx
similarity index 87%
rename from src/client/components/Settings.tsx
rename to src/client/components/Settings/Settings.tsx
index 7aa3b8b..06c0f85 100644
--- a/src/client/components/Settings.tsx
+++ b/src/client/components/Settings/Settings.tsx
@@ -1,9 +1,10 @@
import React, { useState, ReactElement } from "react";
import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
+import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm";
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
-import { ServiceStatuses } from "./ServiceStatuses/ServiceStatuses";
-import settingsObject from "../constants/settings/settingsMenu.json";
+import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
+import settingsObject from "../../constants/settings/settingsMenu.json";
import { isUndefined, map } from "lodash";
interface ISettingsProps {}
@@ -13,7 +14,7 @@ export const Settings = (props: ISettingsProps): ReactElement => {
const settingsContent = [
{
id: "adc-hubs",
- content:
,
+ content:
,
},
{
id: "adc-connection",
@@ -23,17 +24,21 @@ export const Settings = (props: ISettingsProps): ReactElement => {
),
},
+ {
+ id: "qbt-connection",
+ content: (
+
+
+
+ ),
+ },
{
id: "core-service",
- content:
,
+ content: <>a>,
},
{
id: "flushdb",
- content: (
-
-
-
- ),
+ content:
{/* */}
,
},
];
return (
diff --git a/src/client/components/SystemSettings/SystemSettingsForm.tsx b/src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx
similarity index 97%
rename from src/client/components/SystemSettings/SystemSettingsForm.tsx
rename to src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx
index 18a70f1..d19c9e5 100644
--- a/src/client/components/SystemSettings/SystemSettingsForm.tsx
+++ b/src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx
@@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from "react";
-import { flushDb } from "../../actions/settings.actions";
+import { flushDb } from "../../../actions/settings.actions";
import { useDispatch, useSelector } from "react-redux";
export const SystemSettingsForm = (): ReactElement => {
diff --git a/src/client/components/VolumeDetail/PotentialLibraryMatches.tsx b/src/client/components/VolumeDetail/PotentialLibraryMatches.tsx
index 3343d4d..c322828 100644
--- a/src/client/components/VolumeDetail/PotentialLibraryMatches.tsx
+++ b/src/client/components/VolumeDetail/PotentialLibraryMatches.tsx
@@ -2,7 +2,7 @@ import { isArray, map } from "lodash";
import React, { useEffect, ReactElement } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getComicBooksDetailsByIds } from "../../actions/comicinfo.actions";
-import { Card } from "../Carda";
+import { Card } from "../shared/Carda";
import ellipsize from "ellipsize";
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";
import { escapePoundSymbol } from "../../shared/utils/formatting.utils";
diff --git a/src/client/components/VolumeDetail/VolumeDetail.tsx b/src/client/components/VolumeDetail/VolumeDetail.tsx
index 5a4bcfa..d81b1cc 100644
--- a/src/client/components/VolumeDetail/VolumeDetail.tsx
+++ b/src/client/components/VolumeDetail/VolumeDetail.tsx
@@ -9,7 +9,7 @@ import {
} from "../../actions/comicinfo.actions";
import PotentialLibraryMatches from "./PotentialLibraryMatches";
import Masonry from "react-masonry-css";
-import { Card } from "../Carda";
+import { Card } from "../shared/Carda";
import SlidingPane from "react-sliding-pane";
import { convert } from "html-to-text";
import ellipsize from "ellipsize";
diff --git a/src/client/components/Volumes/Volumes.tsx b/src/client/components/Volumes/Volumes.tsx
index 3d81b43..f4d9f1e 100644
--- a/src/client/components/Volumes/Volumes.tsx
+++ b/src/client/components/Volumes/Volumes.tsx
@@ -1,7 +1,7 @@
import React, { ReactElement, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
-import Card from "../Carda";
+import Card from "../shared/Carda";
import T2Table from "../shared/T2Table";
import ellipsize from "ellipsize";
import { convert } from "html-to-text";
diff --git a/src/client/components/Card.tsx b/src/client/components/shared/Card.tsx
similarity index 100%
rename from src/client/components/Card.tsx
rename to src/client/components/shared/Card.tsx
diff --git a/src/client/components/Carda.tsx b/src/client/components/shared/Carda.tsx
similarity index 94%
rename from src/client/components/Carda.tsx
rename to src/client/components/shared/Carda.tsx
index 562ac5e..67adc5b 100644
--- a/src/client/components/Carda.tsx
+++ b/src/client/components/shared/Carda.tsx
@@ -15,7 +15,7 @@ interface ICardProps {
imageStyle?: PropTypes.object;
}
-const renderCard = (props): ReactElement => {
+const renderCard = (props: ICardProps): ReactElement => {
switch (props.orientation) {
case "horizontal":
return (
@@ -85,8 +85,8 @@ const renderCard = (props): ReactElement => {
}
};
-export const Card = (props: ICardProps): ReactElement => {
- return renderCard(props);
-};
+export const Card = React.memo(
+ (props: ICardProps): ReactElement => renderCard(props),
+);
export default Card;
diff --git a/src/client/components/AirDCPPSettings/AirDCPPSettingsForm.tsx b/src/client/components/shared/ConnectionForm/ConnectionForm.tsx
similarity index 51%
rename from src/client/components/AirDCPPSettings/AirDCPPSettingsForm.tsx
rename to src/client/components/shared/ConnectionForm/ConnectionForm.tsx
index 5ba513f..a6b2ccd 100644
--- a/src/client/components/AirDCPPSettings/AirDCPPSettingsForm.tsx
+++ b/src/client/components/shared/ConnectionForm/ConnectionForm.tsx
@@ -1,62 +1,22 @@
-import React, { ReactElement, useCallback, useContext } from "react";
+import React, { ReactElement } from "react";
import { Form, Field } from "react-final-form";
-import { useDispatch } from "react-redux";
-import { saveSettings, deleteSettings } from "../../actions/settings.actions";
-import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
-import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
-import { isUndefined, isEmpty, isNil } from "lodash";
-
-export const AirDCPPSettingsForm = (): ReactElement => {
- const dispatch = useDispatch();
- const airDCPPSettings = useContext(AirDCPPSocketContext);
-
- const hostValidator = (hostname: string): string | null => {
- const hostnameRegex = /[\W]+/gm;
- try {
- if (!isUndefined(hostname)) {
- const matches = hostname.match(hostnameRegex);
- return isNil(matches) && matches.length !== 0
- ? hostname
- : "Invalid hostname; it should not contain special characters";
- }
- } catch {
- return null;
- }
- };
-
- const onSubmit = useCallback(async (values) => {
- try {
- airDCPPSettings.setSettings(values);
- dispatch(
- saveSettings({
- host: values,
- }),
- );
- } catch (error) {
- console.log(error);
- }
- }, []);
- const removeSettings = useCallback(async () => {
- airDCPPSettings.setSettings({});
- dispatch(deleteSettings());
- }, []);
- const validate = async () => {};
- const initFormData = !isUndefined(
- airDCPPSettings.airDCPPState.settings.directConnect,
- )
- ? airDCPPSettings.airDCPPState.settings.directConnect.client.host
- : {};
+import { hostNameValidator } from "../../../shared/utils/validator.utils";
+import { isEmpty } from "lodash";
+export const ConnectionForm = ({
+ initialData,
+ submitHandler,
+ formHeading,
+}): ReactElement => {
return (
<>