diff --git a/.storybook/main.ts b/.storybook/main.ts index b4fe33a..6d68d18 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -7,7 +7,6 @@ const config: StorybookConfig = { "@storybook/addon-essentials", "@storybook/addon-onboarding", "@storybook/addon-interactions", - "@storybook/addon-mdx-gfm" ], framework: { name: "@storybook/react-vite", diff --git a/jsdoc.json b/jsdoc.json index 40760c2..2d59fd5 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -1,34 +1,25 @@ { - "tags": { - "allowUnknownTags": true, - "dictionaries": [ - "jsdoc", - "closure" - ] - }, - "source": { - "include": [ - "./src/client" + "tags": { + "allowUnknownTags": false + }, + "source": { + "include": [ + "./src/client" + ], + "includePattern": "\\.(jsx|js|ts|tsx)$" + }, + "plugins": [ + "plugins/markdown" ], - "includePattern": "\\.(jsx|js|ts|tsx)$" - }, - "plugins": [ - "better-docs/component", - "better-docs/category", - "plugins/markdown", - "node_modules/better-docs/typescript" - ], - "templates": { - "better-docs": { - "name": "ThreeTwo UI components" + "opts": { + "template": "node_modules/tui-jsdoc-template", + "encoding": "utf8", + "destination": "docs/", + "recurse": true, + "verbose": true + }, + "templates": { + "cleverLinks": false, + "monospaceLinks": false } - }, - "opts": { - "destination": "docs/", - "readme": "README.md", - "recurse": true, - "encoding": "utf8", - "verbose": true, - "template": "node_modules/better-docs" - } } \ No newline at end of file diff --git a/package.json b/package.json index b39cb5a..2ecbacf 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,13 @@ "author": "Rishi Ghan", "license": "MIT", "dependencies": { - "@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git", - "@dnd-kit/core": "^4.0.0", - "@dnd-kit/sortable": "^5.0.0", - "@dnd-kit/utilities": "^3.2.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@fortawesome/fontawesome-free": "^6.3.0", - "@redux-devtools/extension": "^3.2.2", + "@redux-devtools/extension": "^3.2.5", "@rollup/plugin-node-resolve": "^15.0.1", + "@tanstack/react-query": "^5.0.5", "@tanstack/react-table": "^8.9.3", "@types/mime-types": "^2.1.0", "@types/react-router-dom": "^5.3.3", @@ -30,9 +30,7 @@ "axios": "^1.3.4", "axios-cache-interceptor": "^1.0.1", "axios-rate-limit": "^1.3.0", - "babel-polyfill": "^6.26.0", - "babel-preset-minify": "^0.5.2", - "better-docs": "^2.7.2", + "babel-plugin-styled-components": "^2.1.4", "date-fns": "^2.28.0", "dayjs": "^1.10.6", "ellipsize": "^0.5.1", @@ -44,7 +42,6 @@ "html-to-text": "^8.1.0", "jsdoc": "^3.6.10", "lodash": "^4.17.21", - "node-sass": "npm:sass", "pretty-bytes": "^5.6.0", "prop-types": "^15.8.1", "qs": "^6.10.5", @@ -59,7 +56,6 @@ "react-loader-spinner": "^4.0.0", "react-masonry-css": "^1.0.16", "react-modal": "^3.15.1", - "react-redux": "^8.0.5", "react-router": "^6.9.0", "react-router-dom": "^6.9.0", "react-select": "^5.3.2", @@ -69,26 +65,26 @@ "react-stickynode": "^4.1.0", "react-textarea-autosize": "^8.3.4", "reapop": "^4.2.1", - "redux-first-history": "^5.1.1", - "redux-socket.io-middleware": "^1.0.4", - "redux-thunk": "^2.4.2", "slick-carousel": "^1.8.1", "socket.io-client": "^4.3.2", "styled-components": "^6.0.7", "threetwo-ui-typings": "^1.0.14", + "vite": "^4.5.0", "vite-plugin-html": "^3.2.0", - "websocket": "^1.0.34" + "websocket": "^1.0.34", + "zustand": "^4.4.6" }, "devDependencies": { - "@storybook/addon-essentials": "^7.3.2", - "@storybook/addon-interactions": "^7.3.2", - "@storybook/addon-links": "^7.3.2", - "@storybook/addon-mdx-gfm": "^7.3.2", + "@storybook/addon-essentials": "^7.4.1", + "@storybook/addon-interactions": "^7.4.1", + "@storybook/addon-links": "^7.4.1", "@storybook/addon-onboarding": "^1.0.8", - "@storybook/blocks": "^7.3.2", - "@storybook/react": "^7.3.2", - "@storybook/react-vite": "^7.3.2", + "@storybook/blocks": "^7.4.1", + "@storybook/react": "^7.4.1", + "@storybook/react-vite": "^7.4.1", "@storybook/testing-library": "^0.2.0", + "@tanstack/eslint-plugin-query": "^5.0.5", + "@tanstack/react-query-devtools": "^5.1.0", "@tsconfig/node14": "^1.0.0", "@types/ellipsize": "^0.1.1", "@types/express": "^4.17.8", @@ -98,17 +94,14 @@ "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-redux": "^7.1.25", - "@typescript-eslint/eslint-plugin": "^4.17.0", - "@typescript-eslint/parser": "^4.17.0", - "babel-plugin-styled-components": "^2.1.4", "body-parser": "^1.19.0", "bulma": "^0.9.4", - "eslint": "^7.22.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-airbnb-base": "^14.2.1", + "docdash": "^2.0.2", + "eslint": "^8.49.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-css-modules": "^2.11.0", "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsdoc": "^46.6.0", "eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-react": "^7.22.0", @@ -117,15 +110,15 @@ "install": "^0.13.0", "jest": "^29.6.3", "nodemon": "^3.0.1", - "npm": "^8.11.0", "prettier": "^2.2.1", "react-refresh": "^0.14.0", "rimraf": "^4.1.3", "sass": "^1.66.1", "storybook": "^7.3.2", - "tslint": "^6.1.3", - "typescript": "^5.1.6", - "vite": "^4.4.9" - + "tui-jsdoc-template": "^1.2.2", + "typescript": "^5.1.6" + }, + "resolutions": { + "styled-components": "^5" } } diff --git a/src/client/actions/settings.actions.tsx b/src/client/actions/settings.actions.tsx index 6b1200d..384783a 100644 --- a/src/client/actions/settings.actions.tsx +++ b/src/client/actions/settings.actions.tsx @@ -3,25 +3,14 @@ import { SETTINGS_OBJECT_FETCHED, SETTINGS_CALL_IN_PROGRESS, SETTINGS_DB_FLUSH_SUCCESS, -} from "../constants/action-types"; + SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED, +} from "../reducers/settings.reducer"; import { LIBRARY_SERVICE_BASE_URI, SETTINGS_SERVICE_BASE_URI, + QBITTORRENT_SERVICE_BASE_URI, } from "../constants/endpoints"; -export const saveSettings = - (settingsPayload, settingsObjectId?: string) => async (dispatch) => { - const result = await axios({ - url: `${SETTINGS_SERVICE_BASE_URI}/saveSettings`, - method: "POST", - data: { settingsPayload, settingsObjectId }, - }); - dispatch({ - type: SETTINGS_OBJECT_FETCHED, - data: result.data, - }); - }; - export const getSettings = (settingsKey?) => async (dispatch) => { const result = await axios({ url: `${SETTINGS_SERVICE_BASE_URI}/getSettings`, @@ -67,3 +56,22 @@ export const flushDb = () => async (dispatch) => { }); } }; + +export const getQBitTorrentClientInfo = (hostInfo) => async (dispatch) => { + await axios.request({ + url: `${QBITTORRENT_SERVICE_BASE_URI}/connect`, + method: "POST", + data: hostInfo, + }); + const qBittorrentClientInfo = await axios.request({ + url: `${QBITTORRENT_SERVICE_BASE_URI}/getClientInfo`, + method: "GET", + }); + + dispatch({ + type: SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED, + data: qBittorrentClientInfo.data, + }); +}; + +export const getProwlarrConnectionInfo = (hostInfo) => async (dispatch) => {}; diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index a620940..6eae757 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,21 +1,18 @@ import React, { ReactElement, useContext, useEffect } from "react"; import Dashboard from "./Dashboard/Dashboard"; -import Import from "./Import"; +import Import from "./Import/Import"; import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer"; import TabulatedContentContainer from "./Library/TabulatedContentContainer"; import LibraryGrid from "./Library/LibraryGrid"; -import Search from "./Search"; -import Settings from "./Settings"; +import Search from "./Search/Search"; +import Settings from "./Settings/Settings"; import VolumeDetail from "./VolumeDetail/VolumeDetail"; import Downloads from "./Downloads/Downloads"; import { Routes, Route } from "react-router-dom"; -import Navbar from "./Navbar"; +import Navbar from "./shared/Navbar"; import "../assets/scss/App.scss"; -import { - AirDCPPSocketContextProvider, - AirDCPPSocketContext, -} from "../context/AirDCPPSocket"; + import { SocketIOProvider } from "../context/SocketIOContext"; import socketIOConnectionInstance from "../shared/socket.io/instance"; import { isEmpty, isNil, isUndefined } from "lodash"; @@ -23,7 +20,6 @@ import { AIRDCPP_DOWNLOAD_PROGRESS_TICK, LS_SINGLE_IMPORT, } from "../constants/action-types"; -import { useDispatch, useSelector } from "react-redux"; /** * Method that initializes an AirDC++ socket connection @@ -31,131 +27,26 @@ import { useDispatch, useSelector } from "react-redux"; * 2. Handles errors in case the connection to AirDC++ is not established or terminated * @returns void */ -const AirDCPPSocketComponent = (): ReactElement => { - const airDCPPConfiguration = useContext(AirDCPPSocketContext); - const dispatch = useDispatch(); - useEffect(() => { - const initializeAirDCPPEventListeners = async () => { - if ( - !isUndefined(airDCPPConfiguration.airDCPPState) && - !isEmpty(airDCPPConfiguration.airDCPPState.settings) && - !isEmpty(airDCPPConfiguration.airDCPPState.socket) - ) { - await airDCPPConfiguration.airDCPPState.socket.addListener( - "queue", - "queue_bundle_added", - async (data) => { - console.log("JEMEN:", data); - }, - ); - // download tick listener - await airDCPPConfiguration.airDCPPState.socket.addListener( - `queue`, - "queue_bundle_tick", - async (downloadProgressData) => { - dispatch({ - type: AIRDCPP_DOWNLOAD_PROGRESS_TICK, - downloadProgressData, - }); - }, - ); - // download complete listener - await airDCPPConfiguration.airDCPPState.socket.addListener( - `queue`, - "queue_bundle_status", - async (bundleData) => { - let count = 0; - if (bundleData.status.completed && bundleData.status.downloaded) { - // dispatch the action for raw import, with the metadata - if (count < 1) { - console.log(`[AirDCPP]: Download complete.`); - dispatch({ - type: LS_SINGLE_IMPORT, - meta: { remote: true }, - data: bundleData, - }); - count += 1; - } - } - }, - ); - console.log( - "[AirDCPP]: Listener registered - listening to queue bundle download ticks", - ); - console.log( - "[AirDCPP]: Listener registered - listening to queue bundle changes", - ); - console.log( - "[AirDCPP]: Listener registered - listening to transfer completion", - ); - } - }; - initializeAirDCPPEventListeners(); - }, [airDCPPConfiguration]); - return <>; -}; export const App = (): ReactElement => { - const dispatch = useDispatch(); - useEffect(() => { - // Check if there is a sessionId in localStorage - const sessionId = localStorage.getItem("sessionId"); - if (!isNil(sessionId)) { - // Resume the session - dispatch({ - type: "RESUME_SESSION", - meta: { remote: true }, - session: { sessionId }, - }); - } else { - // Inititalize the session and persist the sessionId to localStorage - socketIOConnectionInstance.on("sessionInitialized", (sessionId) => { - localStorage.setItem("sessionId", sessionId); - }); - } - }, []); - return ( - - -
- - - - } /> - } /> - } - /> - } /> - } /> - } /> - } - /> - } - /> - } /> - } - /> - } - /> - } - /> - -
-
-
- ); + // useEffect(() => { + // // Check if there is a sessionId in localStorage + // const sessionId = localStorage.getItem("sessionId"); + // if (!isNil(sessionId)) { + // // Resume the session + // dispatch({ + // type: "RESUME_SESSION", + // meta: { remote: true }, + // session: { sessionId }, + // }); + // } else { + // // Inititalize the session and persist the sessionId to localStorage + // socketIOConnectionInstance.on("sessionInitialized", (sessionId) => { + // localStorage.setItem("sessionId", sessionId); + // }); + // } + // }, []); + return <>{/* The rest of your application */}; }; export default App; diff --git a/src/client/components/ComicDetail/ComicDetail.tsx b/src/client/components/ComicDetail/ComicDetail.tsx index ca54014..0777e55 100644 --- a/src/client/components/ComicDetail/ComicDetail.tsx +++ b/src/client/components/ComicDetail/ComicDetail.tsx @@ -1,11 +1,11 @@ import React, { useState, ReactElement, useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; -import Card from "../Carda"; +import Card from "../shared/Carda"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; import { RawFileDetails } from "./RawFileDetails"; -import { ComicVineSearchForm } from "../ComicVineSearchForm"; +import { ComicVineSearchForm } from "./ComicVineSearchForm"; import TabControls from "./TabControls"; import { EditMetadataPanel } from "./EditMetadataPanel"; @@ -198,8 +198,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { }, { id: 4, - icon: , - name: "Acquisition", + icon: , + name: "DC++ Search", content: ( { }, { id: 5, + icon: , + name: "Torrent Search", + content: <>Torrents, + shouldShow: true, + }, + { + id: 6, icon: null, name: !isEmpty(data.data) ? ( Downloads @@ -290,7 +297,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { )} @@ -317,4 +326,4 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { ); }; -export default ComicDetail; \ No newline at end of file +export default ComicDetail; diff --git a/src/client/components/ComicDetail/ComicVineDetails.tsx b/src/client/components/ComicDetail/ComicVineDetails.tsx index 853157b..aa57422 100644 --- a/src/client/components/ComicDetail/ComicVineDetails.tsx +++ b/src/client/components/ComicDetail/ComicVineDetails.tsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import dayjs from "dayjs"; import { isUndefined } from "lodash"; -import Card from "../Carda"; +import Card from "../shared/Carda"; export const ComicVineDetails = (props): ReactElement => { const { data, updatedAt } = props; return ( diff --git a/src/client/components/ComicDetail/ComicVineMatchPanel.tsx b/src/client/components/ComicDetail/ComicVineMatchPanel.tsx index 23e12b4..dd731b2 100644 --- a/src/client/components/ComicDetail/ComicVineMatchPanel.tsx +++ b/src/client/components/ComicDetail/ComicVineMatchPanel.tsx @@ -1,6 +1,6 @@ import React, { ReactElement } from "react"; import { ComicVineSearchForm } from "../ComicVineSearchForm"; -import MatchResult from "../MatchResult"; +import MatchResult from "./MatchResult"; import { isEmpty } from "lodash"; export const ComicVineMatchPanel = (comicVineData): ReactElement => { diff --git a/src/client/components/ComicVineSearchForm.tsx b/src/client/components/ComicDetail/ComicVineSearchForm.tsx similarity index 98% rename from src/client/components/ComicVineSearchForm.tsx rename to src/client/components/ComicDetail/ComicVineSearchForm.tsx index 55dfba7..71b4129 100644 --- a/src/client/components/ComicVineSearchForm.tsx +++ b/src/client/components/ComicDetail/ComicVineSearchForm.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import { Form, Field } from "react-final-form"; import Collapsible from "react-collapsible"; -import { fetchComicVineMatches } from "../actions/fileops.actions"; +import { fetchComicVineMatches } from "../../actions/fileops.actions"; import { useDispatch } from "react-redux"; /** diff --git a/src/client/components/MatchResult.tsx b/src/client/components/ComicDetail/MatchResult.tsx similarity index 98% rename from src/client/components/MatchResult.tsx rename to src/client/components/ComicDetail/MatchResult.tsx index 32f11e3..d057b65 100644 --- a/src/client/components/MatchResult.tsx +++ b/src/client/components/ComicDetail/MatchResult.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { isNil, map } from "lodash"; -import { applyComicVineMatch } from "../actions/comicinfo.actions"; +import { applyComicVineMatch } from "../../actions/comicinfo.actions"; import { convert } from "html-to-text"; import ellipsize from "ellipsize"; diff --git a/src/client/components/ComicDetail/TabControls.tsx b/src/client/components/ComicDetail/TabControls.tsx index e5d82a6..11618cd 100644 --- a/src/client/components/ComicDetail/TabControls.tsx +++ b/src/client/components/ComicDetail/TabControls.tsx @@ -25,12 +25,15 @@ export const TabControls = (props): ReactElement => { > {/* Downloads tab and count badge */} - {id === 5 && + {id === 6 && !isNil(comicBookDetailData.acquisition.directconnect) ? ( - {comicBookDetailData.acquisition.directconnect.downloads.length} + { + comicBookDetailData.acquisition.directconnect.downloads + .length + } ) : ( diff --git a/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx b/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx index ed15699..b1af5f8 100644 --- a/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx +++ b/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useCallback, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { DnD } from "../../DnD"; +import { DnD } from "../../shared/Draggable/DnD"; import { isEmpty } from "lodash"; import Sticky from "react-stickynode"; import SlidingPane from "react-sliding-pane"; diff --git a/src/client/components/Dashboard/Dashboard.tsx b/src/client/components/Dashboard/Dashboard.tsx index 433e850..7b41f0d 100644 --- a/src/client/components/Dashboard/Dashboard.tsx +++ b/src/client/components/Dashboard/Dashboard.tsx @@ -12,114 +12,53 @@ import { } from "../../actions/fileops.actions"; import { getLibraryStatistics } from "../../actions/comicinfo.actions"; import { isEmpty, isNil } from "lodash"; -import Header from "../Header"; +import Header from "../shared/Header"; export const Dashboard = (): ReactElement => { - const dispatch = useDispatch(); - useEffect(() => { - dispatch(fetchVolumeGroups()); - dispatch( - getComicBooks({ - paginationOptions: { - page: 0, - limit: 5, - sort: { updatedAt: "-1" }, - }, - predicate: { "acquisition.source.wanted": false }, - comicStatus: "recent", - }), - ); - dispatch( - getComicBooks({ - paginationOptions: { - page: 0, - limit: 5, - sort: { updatedAt: "-1" }, - }, - predicate: { "acquisition.source.wanted": true }, - comicStatus: "wanted", - }), - ); - dispatch(getLibraryStatistics()); - }, []); - - const recentComics = useSelector( - (state: RootState) => state.fileOps.recentComics, - ); - const wantedComics = useSelector( - (state: RootState) => state.fileOps.wantedComics, - ); - const volumeGroups = useSelector( - (state: RootState) => state.fileOps.comicVolumeGroups, - ); - - const libraryStatistics = useSelector( - (state: RootState) => state.comicInfo.libraryStatistics, - ); + // useEffect(() => { + // dispatch(fetchVolumeGroups()); + // dispatch( + // getComicBooks({ + // paginationOptions: { + // page: 0, + // limit: 5, + // sort: { updatedAt: "-1" }, + // }, + // predicate: { "acquisition.source.wanted": false }, + // comicStatus: "recent", + // }), + // ); + // dispatch( + // getComicBooks({ + // paginationOptions: { + // page: 0, + // limit: 5, + // sort: { updatedAt: "-1" }, + // }, + // predicate: { "acquisition.source.wanted": true }, + // comicStatus: "wanted", + // }), + // ); + // dispatch(getLibraryStatistics()); + // }, []); + // + // const recentComics = useSelector( + // (state: RootState) => state.fileOps.recentComics, + // ); + // const wantedComics = useSelector( + // (state: RootState) => state.fileOps.wantedComics, + // ); + // const volumeGroups = useSelector( + // (state: RootState) => state.fileOps.comicVolumeGroups, + // ); + // + // const libraryStatistics = useSelector( + // (state: RootState) => state.comicInfo.libraryStatistics, + // ); return (

Dashboard

- - {!isEmpty(recentComics) ? ( - <> - {/* Pull List */} - - <> -
-
-
- - - - - - - - - - - - - - - - -
- Pos - Team - Pld -
138
- - - {/* Stats */} - {!isEmpty(libraryStatistics) && ( - - )} - {/* Wanted comics */} - {!isEmpty(wantedComics) && ( - - )} - {/* Recent imports */} - - - {/* Volumes */} - {!isEmpty(volumeGroups) && ( - - )} - - ) : ( - - )}
); diff --git a/src/client/components/Dashboard/PullList.tsx b/src/client/components/Dashboard/PullList.tsx index 44fa41f..033be81 100644 --- a/src/client/components/Dashboard/PullList.tsx +++ b/src/client/components/Dashboard/PullList.tsx @@ -1,7 +1,7 @@ import { isNil, map } from "lodash"; import React, { createRef, ReactElement, useCallback, useEffect } from "react"; -import Card from "../Carda"; -import Header from "../Header"; +import Card from "../shared/Carda"; +import Header from "../shared/Header"; import Masonry from "react-masonry-css"; import { useDispatch, useSelector } from "react-redux"; import { getWeeklyPullList } from "../../actions/comicinfo.actions"; @@ -21,7 +21,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => { useEffect(() => { dispatch( getWeeklyPullList({ - startDate: "2023-8-9", + startDate: "2023-9-9", pageSize: "15", currentPage: "1", }), @@ -91,7 +91,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
+ iconClassNames="fa-solid fa-binoculars mr-2"/>
{/* select week */}
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 ( <>
( -

AirDC++ Connection Information

- +

{formHeading}

+

@@ -68,13 +28,13 @@ export const AirDCPPSettingsForm = (): ReactElement => {

- + {({ input, meta }) => (
{meta.error && meta.touched && ( @@ -91,14 +51,12 @@ export const AirDCPPSettingsForm = (): ReactElement => { name="port" component="input" className="input" - placeholder="AirDC++ port" + placeholder="port" />

-
- -
+

@@ -125,9 +83,6 @@ export const AirDCPPSettingsForm = (): ReactElement => { - - -

@@ -135,28 +90,19 @@ export const AirDCPPSettingsForm = (): ReactElement => {

+

+ +

+

)} /> - {!isEmpty(airDCPPSettings.airDCPPState.socketConnectionInformation) ? ( - - ) : null} - - {!isEmpty(airDCPPSettings.airDCPPState.socketConnectionInformation) ? ( -

- -

- ) : null} ); }; - -export default AirDCPPSettingsForm; diff --git a/src/client/components/Cover.tsx b/src/client/components/shared/Draggable/Cover.tsx similarity index 100% rename from src/client/components/Cover.tsx rename to src/client/components/shared/Draggable/Cover.tsx diff --git a/src/client/components/DnD.tsx b/src/client/components/shared/Draggable/DnD.tsx similarity index 100% rename from src/client/components/DnD.tsx rename to src/client/components/shared/Draggable/DnD.tsx diff --git a/src/client/components/Grid.tsx b/src/client/components/shared/Draggable/Grid.tsx similarity index 100% rename from src/client/components/Grid.tsx rename to src/client/components/shared/Draggable/Grid.tsx diff --git a/src/client/components/SortableCover.tsx b/src/client/components/shared/Draggable/SortableCover.tsx similarity index 100% rename from src/client/components/SortableCover.tsx rename to src/client/components/shared/Draggable/SortableCover.tsx diff --git a/src/client/components/Header.tsx b/src/client/components/shared/Header.tsx similarity index 100% rename from src/client/components/Header.tsx rename to src/client/components/shared/Header.tsx diff --git a/src/client/components/shared/MetadataPanel.tsx b/src/client/components/shared/MetadataPanel.tsx index b12a76d..8fb5e3c 100644 --- a/src/client/components/shared/MetadataPanel.tsx +++ b/src/client/components/shared/MetadataPanel.tsx @@ -2,7 +2,7 @@ import React, { ReactElement } from "react"; import PropTypes from "prop-types"; import ellipsize from "ellipsize"; import prettyBytes from "pretty-bytes"; -import { Card } from "../Carda"; +import { Card } from "../shared/Carda"; import { convert } from "html-to-text"; import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { find, isUndefined } from "lodash"; diff --git a/src/client/components/Navbar.tsx b/src/client/components/shared/Navbar.tsx similarity index 55% rename from src/client/components/Navbar.tsx rename to src/client/components/shared/Navbar.tsx index 7ad6ada..f512a61 100644 --- a/src/client/components/Navbar.tsx +++ b/src/client/components/shared/Navbar.tsx @@ -1,44 +1,11 @@ import React, { useContext } from "react"; -import { SearchBar } from "./GlobalSearchBar/SearchBar"; -import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick"; +import { SearchBar } from "../GlobalSearchBar/SearchBar"; +import { DownloadProgressTick } from "../ComicDetail/DownloadProgressTick"; import { Link } from "react-router-dom"; -import { useSelector } from "react-redux"; import { isUndefined } from "lodash"; import { format, fromUnixTime } from "date-fns"; const Navbar: React.FunctionComponent = (props) => { - const downloadProgressTick = useSelector( - (state: RootState) => state.airdcpp.downloadProgressData, - ); - - const airDCPPSocketConnectionStatus = useSelector( - (state: RootState) => state.airdcpp.isAirDCPPSocketConnected, - ); - const airDCPPSessionInfo = useSelector( - (state: RootState) => state.airdcpp.airDCPPSessionInfo, - ); - const socketDisconnectionReason = useSelector( - (state: RootState) => state.airdcpp.socketDisconnectionReason, - ); - - // Import-related selector hooks - const successfulImportJobCount = useSelector( - (state: RootState) => state.fileOps.successfulJobCount, - ); - const failedImportJobCount = useSelector( - (state: RootState) => state.fileOps.failedJobCount, - ); - - const lastQueueJob = useSelector( - (state: RootState) => state.fileOps.lastQueueJob, - ); - const libraryQueueImportStatus = useSelector( - (state: RootState) => state.fileOps.LSQueueImportStatus, - ); - - const allImportJobResults = useSelector( - (state: RootState) => state.fileOps.importJobStatistics, - ); return (