From dba520b4c133c7f96e93f414e8629495c4721e8e Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Tue, 28 Nov 2023 22:54:45 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B8=20Zustand=20and=20Tanstack=20Query?= =?UTF-8?q?=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ↪️ Removed node-sass, added sass * 🏗️ Refactoring Navbar to read from zustand store * ⬆️ Bumped deps * 🏗️ Refactored AirDC++ session status indicator * 🏗️ Refactored Import page to read from global state * 🏗 Wired up the event emit correctly * 🏗️ Added import queue related state * 🏗 Implemented setQueueAction * 🏗️ Wired up job queue control methods * 🏗️ Added null check and removed useless deps * 🏗️ Refactored the Import page * ↪️ Added cache invalidation to job statistics query * 🏗️ Refactoring the Library page * 🏗️ Fixed pagination and disabled states * ✏️ Changed page to offset To better reflect what we are doing with the pagination controls * 🏗️ Refactoring ComicDetail page and its children * 🏗️ Refactored ComicDetailContainer with useQuery * 🔧 Fixed the error check on Library page * 🏗️ Refactoring AcquisitionPanel * 🏗️ Refactoring the AirDC++ Forms * 🦃 Thanksgiving Day bug fixes * ⬆️ Bumped up Vite to 5.0 * 🔧 Refactoring AcquisitionPanel * 🏗️ Wiring up the DC++ search method * 🏗️ Refactoring AirDC++ search method * 🔎 Added some validation to ADC++ Hubs settings form * 🏗️ Fixed the ADC++ search results * 🏗️ Cleanup of the search results pane --- package.json | 12 +- src/client/actions/airdcpp.actions.tsx | 81 +--- src/client/assets/scss/App.scss | 6 +- src/client/components/App.tsx | 53 +-- .../ComicDetail/AcquisitionPanel.tsx | 350 ++++++++++-------- .../ComicDetail/ActionMenu/Menu.tsx | 6 +- .../components/ComicDetail/ComicDetail.tsx | 65 ++-- .../ComicDetail/ComicDetailContainer.tsx | 42 ++- .../ComicDetail/ComicVineSearchForm.tsx | 4 +- .../components/ComicDetail/DownloadsPanel.tsx | 31 +- .../ComicDetail/EditMetadataPanel.tsx | 10 +- .../components/ComicDetail/MatchResult.tsx | 10 +- .../components/ComicDetail/TabControls.tsx | 21 +- .../ComicDetail/Tabs/ArchiveOperations.tsx | 44 ++- src/client/components/Import/Import.tsx | 231 +++++++----- src/client/components/Library/Library.tsx | 121 +++--- src/client/components/Library/SearchBar.tsx | 37 +- src/client/components/PullList/PullList.tsx | 22 +- .../AirDCPPSettings/AirDCPPHubsForm.tsx | 188 ++++++---- .../AirDCPPSettings/AirDCPPSettingsForm.tsx | 77 ++-- src/client/components/Settings/Settings.tsx | 12 +- .../SystemSettings/SystemSettingsForm.tsx | 27 +- src/client/components/Volumes/Volumes.tsx | 34 +- .../components/WantedComics/WantedComics.tsx | 73 ++-- src/client/components/shared/ErrorPage.tsx | 5 + src/client/components/shared/Navbar.tsx | 134 ++++++- src/client/components/shared/T2Table.tsx | 70 ++-- src/client/context/AirDCPPSocket.tsx | 108 ------ src/client/index.tsx | 23 +- src/client/store/index.ts | 243 ++++++++---- yarn.lock | 299 ++++++++++++--- 31 files changed, 1428 insertions(+), 1011 deletions(-) create mode 100644 src/client/components/shared/ErrorPage.tsx delete mode 100644 src/client/context/AirDCPPSocket.tsx diff --git a/package.json b/package.json index 2ecbacf..e0acf9a 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "final-form-arrays": "^3.0.2", "history": "^5.3.0", "html-to-text": "^8.1.0", + "immer": "^10.0.3", "jsdoc": "^3.6.10", "lodash": "^4.17.21", "pretty-bytes": "^5.6.0", @@ -58,7 +59,7 @@ "react-modal": "^3.15.1", "react-router": "^6.9.0", "react-router-dom": "^6.9.0", - "react-select": "^5.3.2", + "react-select": "^5.8.0", "react-select-async-paginate": "^0.7.2", "react-slick": "^0.29.0", "react-sliding-pane": "^7.1.0", @@ -67,9 +68,9 @@ "reapop": "^4.2.1", "slick-carousel": "^1.8.1", "socket.io-client": "^4.3.2", - "styled-components": "^6.0.7", + "styled-components": "^6.1.0", "threetwo-ui-typings": "^1.0.14", - "vite": "^4.5.0", + "vite": "^5.0.0", "vite-plugin-html": "^3.2.0", "websocket": "^1.0.34", "zustand": "^4.4.6" @@ -113,12 +114,9 @@ "prettier": "^2.2.1", "react-refresh": "^0.14.0", "rimraf": "^4.1.3", - "sass": "^1.66.1", + "sass": "^1.69.5", "storybook": "^7.3.2", "tui-jsdoc-template": "^1.2.2", "typescript": "^5.1.6" - }, - "resolutions": { - "styled-components": "^5" } } diff --git a/src/client/actions/airdcpp.actions.tsx b/src/client/actions/airdcpp.actions.tsx index b63268b..658aa82 100644 --- a/src/client/actions/airdcpp.actions.tsx +++ b/src/client/actions/airdcpp.actions.tsx @@ -33,9 +33,9 @@ interface SearchData { priority: PriorityEnum; } -function sleep(ms: number): Promise { +export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); -} +}; export const toggleAirDCPPSocketConnectionStatus = (status: String, payload?: any) => async (dispatch) => { @@ -59,83 +59,6 @@ export const toggleAirDCPPSocketConnectionStatus = break; } }; -export const search = - (data: SearchData, ADCPPSocket: any, credentials: any) => - async (dispatch) => { - try { - if (!ADCPPSocket.isConnected()) { - await ADCPPSocket(); - } - const instance: SearchInstance = await ADCPPSocket.post("search"); - dispatch({ - type: AIRDCPP_SEARCH_IN_PROGRESS, - }); - - // We want to get notified about every new result in order to make the user experience better - await ADCPPSocket.addListener( - `search`, - "search_result_added", - async (groupedResult) => { - // ...add the received result in the UI - // (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results) - - dispatch({ - type: AIRDCPP_SEARCH_RESULTS_ADDED, - groupedResult, - }); - }, - instance.id, - ); - - // We also want to update the existing items in our list when new hits arrive for the previously listed files/directories - await ADCPPSocket.addListener( - `search`, - "search_result_updated", - async (groupedResult) => { - // ...update properties of the existing result in the UI - dispatch({ - type: AIRDCPP_SEARCH_RESULTS_UPDATED, - groupedResult, - }); - }, - instance.id, - ); - - // We need to show something to the user in case the search won't yield any results so that he won't be waiting forever) - // Wait for 5 seconds for any results to arrive after the searches were sent to the hubs - await ADCPPSocket.addListener( - `search`, - "search_hub_searches_sent", - async (searchInfo) => { - await sleep(5000); - - // Check the number of received results (in real use cases we should know that even without calling the API) - const currentInstance = await ADCPPSocket.get( - `search/${instance.id}`, - ); - if (currentInstance.result_count === 0) { - // ...nothing was received, show an informative message to the user - console.log("No more search results."); - } - - // The search can now be considered to be "complete" - // If there's an "in progress" indicator in the UI, that could also be disabled here - dispatch({ - type: AIRDCPP_HUB_SEARCHES_SENT, - searchInfo, - instance, - }); - }, - instance.id, - ); - // Finally, perform the actual search - await ADCPPSocket.post(`search/${instance.id}/hub_search`, data); - } catch (error) { - console.log(error); - throw error; - } - }; - export const downloadAirDCPPItem = ( searchInstanceId: Number, diff --git a/src/client/assets/scss/App.scss b/src/client/assets/scss/App.scss index 97e0634..0833103 100644 --- a/src/client/assets/scss/App.scss +++ b/src/client/assets/scss/App.scss @@ -13,7 +13,9 @@ $size-9: 0.7rem; $flexSize: 4em; $boxSpacing: 1em; $colorText: #404646; - +body { + background: #fffcc; +} .is-size-8 { font-size: $size-8; } @@ -422,7 +424,7 @@ pre { width: 100%; padding: 25px 0 15px 0; position: sticky; - z-index:9999; + z-index: 9999; background: #fffffc; top: 50px; } diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 6eae757..8e133b4 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,52 +1,15 @@ -import React, { ReactElement, useContext, useEffect } from "react"; -import Dashboard from "./Dashboard/Dashboard"; -import Import from "./Import/Import"; -import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer"; -import TabulatedContentContainer from "./Library/TabulatedContentContainer"; -import LibraryGrid from "./Library/LibraryGrid"; -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 React, { ReactElement } from "react"; +import { Outlet } from "react-router-dom"; import Navbar from "./shared/Navbar"; import "../assets/scss/App.scss"; -import { SocketIOProvider } from "../context/SocketIOContext"; -import socketIOConnectionInstance from "../shared/socket.io/instance"; -import { isEmpty, isNil, isUndefined } from "lodash"; -import { - AIRDCPP_DOWNLOAD_PROGRESS_TICK, - LS_SINGLE_IMPORT, -} from "../constants/action-types"; - -/** - * Method that initializes an AirDC++ socket connection - * 1. Initializes event listeners for download init, tick and complete events - * 2. Handles errors in case the connection to AirDC++ is not established or terminated - * @returns void - */ - export const App = (): ReactElement => { - // 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 */}; + return ( + <> + + + + ); }; export default App; diff --git a/src/client/components/ComicDetail/AcquisitionPanel.tsx b/src/client/components/ComicDetail/AcquisitionPanel.tsx index 5570daf..2b25188 100644 --- a/src/client/components/ComicDetail/AcquisitionPanel.tsx +++ b/src/client/components/ComicDetail/AcquisitionPanel.tsx @@ -1,21 +1,18 @@ -import React, { - useCallback, - useContext, - ReactElement, - useEffect, - useState, -} from "react"; +import React, { useCallback, ReactElement, useEffect, useState } from "react"; import { - search, downloadAirDCPPItem, getBundlesForComic, + sleep, } from "../../actions/airdcpp.actions"; -import { useDispatch, useSelector } from "react-redux"; +import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings"; import { RootState, SearchInstance } from "threetwo-ui-typings"; import ellipsize from "ellipsize"; import { Form, Field } from "react-final-form"; +import { difference } from "../../shared/utils/object.utils"; import { isEmpty, isNil, map } from "lodash"; -import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; +import { useStore } from "../../store"; +import { useShallow } from "zustand/react/shallow"; +import { useQuery } from "@tanstack/react-query"; interface IAcquisitionPanelProps { query: any; @@ -27,107 +24,196 @@ interface IAcquisitionPanelProps { export const AcquisitionPanel = ( props: IAcquisitionPanelProps, ): ReactElement => { + const { + airDCPPSocketInstance, + airDCPPClientConfiguration, + airDCPPSessionInformation, + } = useStore( + useShallow((state) => ({ + airDCPPSocketInstance: state.airDCPPSocketInstance, + airDCPPClientConfiguration: state.airDCPPClientConfiguration, + airDCPPSessionInformation: state.airDCPPSessionInformation, + })), + ); + + /** + * Get the hubs list from an AirDCPP Socket + */ + const { data: hubs } = useQuery({ + queryKey: ["hubs"], + queryFn: async () => await airDCPPSocketInstance.get(`hubs`), + }); + const issueName = props.query.issue.name || ""; // const { settings } = props; const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " "); // Selectors for picking state - const airDCPPSearchResults = useSelector((state: RootState) => { - return state.airdcpp.searchResults; - }); - const isAirDCPPSearchInProgress = useSelector( - (state: RootState) => state.airdcpp.isAirDCPPSearchInProgress, - ); - const searchInfo = useSelector( - (state: RootState) => state.airdcpp.searchInfo, - ); - const searchInstance: SearchInstance = useSelector( - (state: RootState) => state.airdcpp.searchInstance, - ); + // const airDCPPSearchResults = useSelector((state: RootState) => { + // return state.airdcpp.searchResults; + // }); + // const isAirDCPPSearchInProgress = useSelector( + // (state: RootState) => state.airdcpp.isAirDCPPSearchInProgress, + // ); + // const searchInfo = useSelector( + // (state: RootState) => state.airdcpp.searchInfo, + // ); + // const searchInstance: SearchInstance = useSelector( + // (state: RootState) => state.airdcpp.searchInstance, + // ); // const settings = useSelector((state: RootState) => state.settings.data); - const airDCPPConfiguration = useContext(AirDCPPSocketContext); - - const dispatch = useDispatch(); + // const airDCPPConfiguration = useContext(AirDCPPSocketContext); + interface SearchData { + query: Pick & Partial>; + hub_urls: string[] | undefined | null; + priority: PriorityEnum; + } const [dcppQuery, setDcppQuery] = useState({}); + const [airDCPPSearchResults, setAirDCPPSearchResults] = useState([]); + // Construct a AirDC++ query based on metadata inferred, upon component mount + // Pre-populate the search input with the search string, so that + // All the user has to do is hit "Search AirDC++" useEffect(() => { - if (!isEmpty(airDCPPConfiguration.airDCPPState.settings)) { - // AirDC++ search query - const dcppSearchQuery = { - query: { - pattern: `${sanitizedIssueName.replace(/#/g, "")}`, - extensions: ["cbz", "cbr", "cb7"], - }, - hub_urls: map( - airDCPPConfiguration.airDCPPState.settings.directConnect.client.hubs, - (item) => item.value, - ), - priority: 5, - }; - setDcppQuery(dcppSearchQuery); - } - }, [airDCPPConfiguration]); + // AirDC++ search query + const dcppSearchQuery = { + query: { + pattern: `${sanitizedIssueName.replace(/#/g, "")}`, + extensions: ["cbz", "cbr", "cb7"], + }, + hub_urls: map(hubs, (item) => item.value), + priority: 5, + }; + setDcppQuery(dcppSearchQuery); + }, []); - const getDCPPSearchResults = useCallback( - async (searchQuery) => { - const manualQuery = { - query: { - pattern: `${searchQuery.issueName}`, - extensions: ["cbz", "cbr", "cb7"], + const search = async (data: SearchData, ADCPPSocket: any) => { + try { + if (!ADCPPSocket.isConnected()) { + await ADCPPSocket(); + } + const instance: SearchInstance = await ADCPPSocket.post("search"); + // dispatch({ + // type: AIRDCPP_SEARCH_IN_PROGRESS, + // }); + + // We want to get notified about every new result in order to make the user experience better + await ADCPPSocket.addListener( + `search`, + "search_result_added", + async (groupedResult) => { + // ...add the received result in the UI + // (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results) + setAirDCPPSearchResults((state) => [...state, groupedResult]); }, - hub_urls: map( - airDCPPConfiguration.airDCPPState.settings.directConnect.client.hubs, - (item) => item.value, - ), - priority: 5, - }; - dispatch( - search(manualQuery, airDCPPConfiguration.airDCPPState.socket, { - username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, - password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, - }), + instance.id, ); - }, - [dispatch, airDCPPConfiguration], - ); + + // We also want to update the existing items in our list when new hits arrive for the previously listed files/directories + await ADCPPSocket.addListener( + `search`, + "search_result_updated", + async (groupedResult) => { + // ...update properties of the existing result in the UI + const bundleToUpdateIndex = airDCPPSearchResults?.findIndex( + (bundle) => bundle.result.id === groupedResult.result.id, + ); + const updatedState = [...airDCPPSearchResults]; + if ( + !isNil(difference(updatedState[bundleToUpdateIndex], groupedResult)) + ) { + updatedState[bundleToUpdateIndex] = groupedResult; + } + setAirDCPPSearchResults((state) => [...state, ...updatedState]); + }, + instance.id, + ); + + // We need to show something to the user in case the search won't yield any results so that he won't be waiting forever) + // Wait for 5 seconds for any results to arrive after the searches were sent to the hubs + await ADCPPSocket.addListener( + `search`, + "search_hub_searches_sent", + async (searchInfo) => { + await sleep(5000); + + // Check the number of received results (in real use cases we should know that even without calling the API) + const currentInstance = await ADCPPSocket.get( + `search/${instance.id}`, + ); + if (currentInstance.result_count === 0) { + // ...nothing was received, show an informative message to the user + console.log("No more search results."); + } + + // The search can now be considered to be "complete" + // If there's an "in progress" indicator in the UI, that could also be disabled here + // dispatch({ + // type: AIRDCPP_HUB_SEARCHES_SENT, + // searchInfo, + // instance, + // }); + }, + instance.id, + ); + // Finally, perform the actual search + await ADCPPSocket.post(`search/${instance.id}/hub_search`, data); + } catch (error) { + console.log(error); + throw error; + } + }; + const getDCPPSearchResults = async (searchQuery) => { + const manualQuery = { + query: { + pattern: `${searchQuery.issueName}`, + extensions: ["cbz", "cbr", "cb7"], + }, + hub_urls: map(hubs, (hub) => hub.hub_url), + priority: 5, + }; + + search(manualQuery, airDCPPSocketInstance); + }; // download via AirDC++ const downloadDCPPResult = useCallback( (searchInstanceId, resultId, name, size, type) => { - dispatch( - downloadAirDCPPItem( - searchInstanceId, - resultId, - props.comicObjectId, - name, - size, - type, - airDCPPConfiguration.airDCPPState.socket, - { - username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, - password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, - }, - ), - ); + // dispatch( + // downloadAirDCPPItem( + // searchInstanceId, + // resultId, + // props.comicObjectId, + // name, + // size, + // type, + // airDCPPConfiguration.airDCPPState.socket, + // { + // username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, + // password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, + // }, + // ), + // ); // this is to update the download count badge on the downloads tab - dispatch( - getBundlesForComic( - props.comicObjectId, - airDCPPConfiguration.airDCPPState.socket, - { - username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, - password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, - }, - ), - ); + // dispatch( + // getBundlesForComic( + // props.comicObjectId, + // airDCPPConfiguration.airDCPPState.socket, + // { + // username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, + // password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, + // }, + // ), + // ); }, - [airDCPPConfiguration], + [], ); + console.log("yaman", airDCPPSearchResults); return ( <>
- {!isEmpty(airDCPPConfiguration.airDCPPState.socket) ? ( + {!isEmpty(airDCPPSocketInstance) ? (
- - Use this to perform a manual search. - +
); }} @@ -161,9 +245,9 @@ export const AcquisitionPanel = ( ); @@ -323,10 +367,22 @@ export const AcquisitionPanel = (
- Searching via AirDC++ is still in{" "} - alpha. Some searches may take arbitrarily long, - or may not work at all. Searches from ADCS hubs are - more reliable than NMDCS ones. +

+ 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. +

+
+
+ +
+
+

+ Searching via AirDC++ is still in + alpha. Some searches may take arbitrarily + long, or may not work at all. Searches from ADCS + hubs are more reliable than NMDCS ones. +

@@ -336,4 +392,4 @@ export const AcquisitionPanel = ( ); }; -export default AcquisitionPanel; \ No newline at end of file +export default AcquisitionPanel; diff --git a/src/client/components/ComicDetail/ActionMenu/Menu.tsx b/src/client/components/ComicDetail/ActionMenu/Menu.tsx index 829081e..39bda45 100644 --- a/src/client/components/ComicDetail/ActionMenu/Menu.tsx +++ b/src/client/components/ComicDetail/ActionMenu/Menu.tsx @@ -1,6 +1,5 @@ import { filter, isEmpty, isNil, isUndefined } from "lodash"; import React, { ReactElement, useCallback } from "react"; -import { useSelector, useDispatch } from "react-redux"; import Select, { components } from "react-select"; import { fetchComicVineMatches } from "../../../actions/fileops.actions"; import { refineQuery } from "filename-parser"; @@ -8,7 +7,6 @@ import { refineQuery } from "filename-parser"; export const Menu = (props): ReactElement => { const { data } = props; const { setSlidingPanelContentId, setVisible } = props.handlers; - const dispatch = useDispatch(); const openDrawerWithCVMatches = useCallback(() => { let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery; let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery; @@ -18,10 +16,10 @@ export const Menu = (props): ReactElement => { } else if (!isEmpty(data.sourcedMetadata)) { issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name); } - dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery)); + // dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery)); setSlidingPanelContentId("CVMatches"); setVisible(true); - }, [dispatch, data]); + }, [data]); const openEditMetadataPanel = useCallback(() => { setSlidingPanelContentId("editComicBookMetadata"); diff --git a/src/client/components/ComicDetail/ComicDetail.tsx b/src/client/components/ComicDetail/ComicDetail.tsx index 0777e55..92b1ef4 100644 --- a/src/client/components/ComicDetail/ComicDetail.tsx +++ b/src/client/components/ComicDetail/ComicDetail.tsx @@ -1,5 +1,4 @@ import React, { useState, ReactElement, useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; import Card from "../shared/Carda"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; @@ -47,6 +46,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { rawFileDetails, inferredMetadata, sourcedMetadata: { comicvine, locg, comicInfo }, + acquisition, }, userSettings, } = data; @@ -55,34 +55,34 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { const [slidingPanelContentId, setSlidingPanelContentId] = useState(""); const [modalIsOpen, setIsOpen] = useState(false); - const comicVineSearchResults = useSelector( - (state: RootState) => state.comicInfo.searchResults, - ); - const comicVineSearchQueryObject = useSelector( - (state: RootState) => state.comicInfo.searchQuery, - ); - const comicVineAPICallProgress = useSelector( - (state: RootState) => state.comicInfo.inProgress, - ); - - const extractedComicBook = useSelector( - (state: RootState) => state.fileOps.extractedComicBookArchive.reading, - ); + // const comicVineSearchResults = useSelector( + // (state: RootState) => state.comicInfo.searchResults, + // ); + // const comicVineSearchQueryObject = useSelector( + // (state: RootState) => state.comicInfo.searchQuery, + // ); + // const comicVineAPICallProgress = useSelector( + // (state: RootState) => state.comicInfo.inProgress, + // ); + // + // const extractedComicBook = useSelector( + // (state: RootState) => state.fileOps.extractedComicBookArchive.reading, + // ); const { comicObjectId } = useParams<{ comicObjectId: string }>(); - const dispatch = useDispatch(); + // const dispatch = useDispatch(); const openModal = useCallback((filePath) => { setIsOpen(true); - dispatch( - extractComicArchive(filePath, { - type: "full", - purpose: "reading", - imageResizeOptions: { - baseWidth: 1024, - }, - }), - ); + // dispatch( + // extractComicArchive(filePath, { + // type: "full", + // purpose: "reading", + // imageResizeOptions: { + // baseWidth: 1024, + // }, + // }), + // ); }, []); const afterOpenModal = useCallback((things) => { @@ -100,7 +100,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { CVMatches: { content: (props) => ( <> -
+ {/*
@@ -134,7 +134,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { />
- )} + )} */} ), }, @@ -193,7 +193,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { id: 3, icon: , name: "Archive Operations", - content: , + content: <>, + /* + */ shouldShow: areRawFileDetailsAvailable, }, { @@ -285,7 +287,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { Read - { }} /> )} - + */} )} - {} + { - const comicBookDetailData = useSelector( - (state: RootState) => state.comicInfo.comicBookDetail, - ); - const dispatch = useDispatch(); - const { comicObjectId } = useParams<{ comicObjectId: string }>(); + const { + data: comicBookDetailData, + isLoading, + isError, + } = useQuery({ + queryKey: [], + queryFn: async () => + await axios({ + url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`, + method: "POST", + data: { + id: comicObjectId, + }, + }), + }); + console.log(comicBookDetailData); useEffect(() => { - dispatch(getComicBookDetailById(comicObjectId)); + // dispatch(getComicBookDetailById(comicObjectId)); // dispatch(getSettings()); - }, [dispatch]); - return !isEmpty(comicBookDetailData) ? ( - - ) : null; + }, []); + + { + isError && <>Error; + } + { + isLoading && <>Loading...; + } + return ( + comicBookDetailData?.data && + ); }; diff --git a/src/client/components/ComicDetail/ComicVineSearchForm.tsx b/src/client/components/ComicDetail/ComicVineSearchForm.tsx index 71b4129..7c08a01 100644 --- a/src/client/components/ComicDetail/ComicVineSearchForm.tsx +++ b/src/client/components/ComicDetail/ComicVineSearchForm.tsx @@ -2,7 +2,6 @@ import React, { useCallback } from "react"; import { Form, Field } from "react-final-form"; import Collapsible from "react-collapsible"; import { fetchComicVineMatches } from "../../actions/fileops.actions"; -import { useDispatch } from "react-redux"; /** * Component for performing search against ComicVine @@ -14,7 +13,6 @@ import { useDispatch } from "react-redux"; * ) */ export const ComicVineSearchForm = (data) => { - const dispatch = useDispatch(); const onSubmit = useCallback((value) => { const userInititatedQuery = { inferredIssueDetails: { @@ -24,7 +22,7 @@ export const ComicVineSearchForm = (data) => { year: value.issueYear, }, }; - dispatch(fetchComicVineMatches(data, userInititatedQuery)); + // dispatch(fetchComicVineMatches(data, userInititatedQuery)); }, []); const validate = () => { return true; diff --git a/src/client/components/ComicDetail/DownloadsPanel.tsx b/src/client/components/ComicDetail/DownloadsPanel.tsx index 21d53f6..1898cd7 100644 --- a/src/client/components/ComicDetail/DownloadsPanel.tsx +++ b/src/client/components/ComicDetail/DownloadsPanel.tsx @@ -1,12 +1,10 @@ import React, { useEffect, useContext, ReactElement } from "react"; import { getBundlesForComic } from "../../actions/airdcpp.actions"; -import { useDispatch, useSelector } from "react-redux"; import { RootState } from "threetwo-ui-typings"; import { isEmpty, isNil, map } from "lodash"; import prettyBytes from "pretty-bytes"; import dayjs from "dayjs"; import ellipsize from "ellipsize"; -import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; interface IDownloadsPanelProps { data: any; @@ -16,34 +14,33 @@ interface IDownloadsPanelProps { export const DownloadsPanel = ( props: IDownloadsPanelProps, ): ReactElement | null => { - const bundles = useSelector((state: RootState) => { - return state.airdcpp.bundles; - }); - - // AirDCPP Socket initialization - const userSettings = useSelector((state: RootState) => state.settings.data); - const airDCPPConfiguration = useContext(AirDCPPSocketContext); + // const bundles = useSelector((state: RootState) => { + // return state.airdcpp.bundles; + // }); + // + // // AirDCPP Socket initialization + // const userSettings = useSelector((state: RootState) => state.settings.data); + // const airDCPPConfiguration = useContext(AirDCPPSocketContext); const { airDCPPState: { socket, settings }, } = airDCPPConfiguration; - const dispatch = useDispatch(); // Fetch the downloaded files and currently-downloading file(s) from AirDC++ useEffect(() => { try { if (!isEmpty(userSettings)) { - dispatch( - getBundlesForComic(props.comicObjectId, socket, { - username: `${settings.directConnect.client.host.username}`, - password: `${settings.directConnect.client.host.password}`, - }), - ); + // dispatch( + // getBundlesForComic(props.comicObjectId, socket, { + // username: `${settings.directConnect.client.host.username}`, + // password: `${settings.directConnect.client.host.password}`, + // }), + // ); } } catch (error) { throw new Error(error); } - }, [dispatch, airDCPPConfiguration]); + }, [airDCPPConfiguration]); const Bundles = (props) => { return !isEmpty(props.data) ? ( diff --git a/src/client/components/ComicDetail/EditMetadataPanel.tsx b/src/client/components/ComicDetail/EditMetadataPanel.tsx index 77d881d..b911370 100644 --- a/src/client/components/ComicDetail/EditMetadataPanel.tsx +++ b/src/client/components/ComicDetail/EditMetadataPanel.tsx @@ -1,5 +1,4 @@ import React, { ReactElement, useCallback, useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; import { Form, Field } from "react-final-form"; import arrayMutators from "final-form-arrays"; import { FieldArray } from "react-final-form-arrays"; @@ -9,7 +8,7 @@ import TextareaAutosize from "react-textarea-autosize"; export const EditMetadataPanel = (props): ReactElement => { const validate = async () => {}; const onSubmit = async () => {}; - + const AsyncSelectPaginateAdapter = ({ input, ...rest }) => { return ( { /> ); }; - const rawFileDetails = useSelector( - (state: RootState) => state.comicInfo.comicBookDetail.rawFileDetails.name, - ); - const dispatch = useDispatch(); + // const rawFileDetails = useSelector( + // (state: RootState) => state.comicInfo.comicBookDetail.rawFileDetails.name, + // ); return ( <> diff --git a/src/client/components/ComicDetail/MatchResult.tsx b/src/client/components/ComicDetail/MatchResult.tsx index d057b65..7b13ba0 100644 --- a/src/client/components/ComicDetail/MatchResult.tsx +++ b/src/client/components/ComicDetail/MatchResult.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { isNil, map } from "lodash"; import { applyComicVineMatch } from "../../actions/comicinfo.actions"; import { convert } from "html-to-text"; @@ -15,12 +14,11 @@ const handleBrokenImage = (e) => { }; export const MatchResult = (props: MatchResultProps) => { - const dispatch = useDispatch(); const applyCVMatch = useCallback( - (match, comicObjectId) => { - dispatch(applyComicVineMatch(match, comicObjectId)); - }, - [dispatch], + // (match, comicObjectId) => { + // dispatch(applyComicVineMatch(match, comicObjectId)); + // }, + [], ); return ( <> diff --git a/src/client/components/ComicDetail/TabControls.tsx b/src/client/components/ComicDetail/TabControls.tsx index 11618cd..782a4c4 100644 --- a/src/client/components/ComicDetail/TabControls.tsx +++ b/src/client/components/ComicDetail/TabControls.tsx @@ -1,18 +1,17 @@ import React, { ReactElement, useEffect, useState } from "react"; import { isEmpty, isNil } from "lodash"; -import { useSelector } from "react-redux"; export const TabControls = (props): ReactElement => { - const comicBookDetailData = useSelector( - (state: RootState) => state.comicInfo.comicBookDetail, - ); - const { filteredTabs } = props; - + // const comicBookDetailData = useSelector( + // (state: RootState) => state.comicInfo.comicBookDetail, + // ); + const { filteredTabs, acquisition } = props; const [active, setActive] = useState(filteredTabs[0].id); useEffect(() => { setActive(filteredTabs[0].id); - }, [comicBookDetailData]); + }, [acquisition]); + console.log(filteredTabs); return ( <>
@@ -25,15 +24,11 @@ export const TabControls = (props): ReactElement => { > {/* Downloads tab and count badge */} - {id === 6 && - !isNil(comicBookDetailData.acquisition.directconnect) ? ( + {id === 6 && !isNil(acquisition.directconnect) ? ( - { - comicBookDetailData.acquisition.directconnect.downloads - .length - } + {acquisition.directconnect.downloads.length} ) : ( diff --git a/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx b/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx index b1af5f8..3eb94e2 100644 --- a/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx +++ b/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx @@ -1,5 +1,4 @@ import React, { ReactElement, useCallback, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; import { DnD } from "../../shared/Draggable/DnD"; import { isEmpty } from "lodash"; import Sticky from "react-stickynode"; @@ -10,28 +9,27 @@ import { Canvas } from "../../shared/Canvas"; export const ArchiveOperations = (props): ReactElement => { const { data } = props; - const isComicBookExtractionInProgress = useSelector( - (state: RootState) => state.fileOps.comicBookExtractionInProgress, - ); - const extractedComicBookArchive = useSelector( - (state: RootState) => state.fileOps.extractedComicBookArchive.analysis, - ); + // const isComicBookExtractionInProgress = useSelector( + // (state: RootState) => state.fileOps.comicBookExtractionInProgress, + // ); + // const extractedComicBookArchive = useSelector( + // (state: RootState) => state.fileOps.extractedComicBookArchive.analysis, + // ); + // + // const imageAnalysisResult = useSelector((state: RootState) => { + // return state.fileOps.imageAnalysisResults; + // }); - const imageAnalysisResult = useSelector((state: RootState) => { - return state.fileOps.imageAnalysisResults; - }); - - const dispatch = useDispatch(); const unpackComicArchive = useCallback(() => { - dispatch( - extractComicArchive(data.rawFileDetails.filePath, { - type: "full", - purpose: "analysis", - imageResizeOptions: { - baseWidth: 275, - }, - }), - ); + // dispatch( + // extractComicArchive(data.rawFileDetails.filePath, { + // type: "full", + // purpose: "analysis", + // imageResizeOptions: { + // baseWidth: 275, + // }, + // }), + // ); }, []); // sliding panel config @@ -64,7 +62,7 @@ export const ArchiveOperations = (props): ReactElement => { // sliding panel handlers const openImageAnalysisPanel = useCallback((imageFilePath) => { setSlidingPanelContentId("imageAnalysis"); - dispatch(analyzeImage(imageFilePath)); + // dispatch(analyzeImage(imageFilePath)); setCurrentImage(imageFilePath); setVisible(true); }, []); @@ -126,4 +124,4 @@ export const ArchiveOperations = (props): ReactElement => { ); }; -export default ArchiveOperations; \ No newline at end of file +export default ArchiveOperations; diff --git a/src/client/components/Import/Import.tsx b/src/client/components/Import/Import.tsx index d961df6..de96c86 100644 --- a/src/client/components/Import/Import.tsx +++ b/src/client/components/Import/Import.tsx @@ -1,14 +1,12 @@ import React, { ReactElement, useCallback, useEffect } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { - fetchComicBookMetadata, - getImportJobResultStatistics, - setQueueControl, -} 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"; import { isEmpty, isNil, isUndefined } from "lodash"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useStore } from "../../store"; +import { useShallow } from "zustand/react/shallow"; +import axios from "axios"; interface IProps { matches?: unknown; @@ -18,10 +16,7 @@ interface IProps { } /** - * Returns the average of two numbers. - * - * @remarks - * This method is part of the {@link core-library#Statistics | Statistics subsystem}. + * Component to facilitate the import of comics to the ThreeTwo library * * @param x - The first input number * @param y - The second input number @@ -31,41 +26,70 @@ interface IProps { */ export const Import = (props: IProps): ReactElement => { - const dispatch = useDispatch(); - const successfulImportJobCount = useSelector( - (state: RootState) => state.fileOps.successfulJobCount, - ); - const failedImportJobCount = useSelector( - (state: RootState) => state.fileOps.failedJobCount, + const queryClient = useQueryClient(); + const { importJobQueue, socketIOInstance } = useStore( + useShallow((state) => ({ + importJobQueue: state.importJobQueue, + socketIOInstance: state.socketIOInstance, + })), ); - const lastQueueJob = useSelector( - (state: RootState) => state.fileOps.lastQueueJob, - ); - const libraryQueueImportStatus = useSelector( - (state: RootState) => state.fileOps.LSQueueImportStatus, - ); + const sessionId = localStorage.getItem("sessionId"); + const { mutate: initiateImport } = useMutation({ + mutationFn: async () => + await axios.request({ + url: `http://localhost:3000/api/library/newImport`, + method: "POST", + data: { sessionId }, + }), + }); - const allImportJobResults = useSelector( - (state: RootState) => state.fileOps.importJobStatistics, - ); + const { data, isError, isLoading } = useQuery({ + queryKey: ["allImportJobResults"], + queryFn: async () => + await axios({ + method: "GET", + url: "http://localhost:3000/api/jobqueue/getJobResultStatistics", + }), + }); - const initiateImport = useCallback(() => { - if (typeof props.path !== "undefined") { - dispatch(fetchComicBookMetadata(props.path)); - } - }, [dispatch]); - - const toggleQueue = useCallback( - (queueAction: string, queueStatus: string) => { - dispatch(setQueueControl(queueAction, queueStatus)); - }, - [], - ); - useEffect(() => { - dispatch(getImportJobResultStatistics()); - }, []); + // 1a. Act on each comic issue successfully imported/failed, as indicated + // by the LS_COVER_EXTRACTED/LS_COVER_EXTRACTION_FAILED events + socketIOInstance.on("LS_COVER_EXTRACTED", (data) => { + const { completedJobCount, importResult } = data; + importJobQueue.setJobCount("successful", completedJobCount); + importJobQueue.setMostRecentImport(importResult.rawFileDetails.name); + }); + socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => { + const { failedJobCount } = data; + importJobQueue.setJobCount("failed", failedJobCount); + }); + // 1b. Clear the localStorage sessionId upon receiving the + // LS_IMPORT_QUEUE_DRAINED event + socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => { + localStorage.removeItem("sessionId"); + importJobQueue.setStatus("drained"); + queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] }); + }); + const toggleQueue = (queueAction: string, queueStatus: string) => { + socketIOInstance.emit( + "call", + "socket.setQueueStatus", + { + queueAction, + queueStatus, + }, + (data) => console.log(data), + ); + }; + /** + * Method to render import job queue pause/resume controls on the UI + * + * @param status The `string` status (either `"pause"` or `"resume"`) + * @returns ReactElement A ` @@ -84,7 +111,10 @@ export const Import = (props: IProps): ReactElement => {
@@ -123,12 +153,15 @@ export const Import = (props: IProps): ReactElement => {

- {libraryQueueImportStatus !== "drained" && - !isUndefined(libraryQueueImportStatus) && ( + + {importJobQueue.status !== "drained" && + !isUndefined(importJobQueue.status) && ( <> @@ -152,29 +186,29 @@ export const Import = (props: IProps): ReactElement => { - + @@ -182,53 +216,58 @@ export const Import = (props: IProps): ReactElement => {
- {successfulImportJobCount > 0 && ( + {importJobQueue.successfulJobCount > 0 && (
- {successfulImportJobCount} + {importJobQueue.successfulJobCount}
)}
- {failedImportJobCount > 0 && ( + {importJobQueue.failedJobCount > 0 && (
- {failedImportJobCount} + {importJobQueue.failedJobCount}
)}
{renderQueueControls(libraryQueueImportStatus)}{renderQueueControls(importJobQueue.status)} - {libraryQueueImportStatus !== undefined ? ( + {importJobQueue.status !== undefined ? ( - {libraryQueueImportStatus} + {importJobQueue.status} ) : null}
Imported{" "} - {lastQueueJob} + + {importJobQueue.mostRecentImport} + )} {/* Past imports */} - -

Past Imports

- - - - - - - - - - - - {allImportJobResults.map((jobResult, id) => { - return ( - - - - - + {!isLoading && !isEmpty(data?.data) && ( + <> +

Past Imports

+
Time StartedSession IdImportedFailed
- {format( - new Date(jobResult.earliestTimestamp), - "EEEE, hh:mma, do LLLL Y", - )} - - - {jobResult.sessionId} - - - - {jobResult.completedJobs} - - - - {jobResult.failedJobs} - -
+ + + + + + - ); - })} - -
Time StartedSession IdImportedFailed
+ + + + {data?.data.map((jobResult, id) => { + return ( + + + {format( + new Date(jobResult.earliestTimestamp), + "EEEE, hh:mma, do LLLL Y", + )} + + + + {jobResult.sessionId} + + + + + {jobResult.completedJobs} + + + + + {jobResult.failedJobs} + + + + ); + })} + + + + )}
); diff --git a/src/client/components/Library/Library.tsx b/src/client/components/Library/Library.tsx index 19f3084..42f40f7 100644 --- a/src/client/components/Library/Library.tsx +++ b/src/client/components/Library/Library.tsx @@ -1,12 +1,12 @@ -import React, { useMemo, ReactElement, useCallback, useEffect } from "react"; +import React, { useMemo, ReactElement, useState } from "react"; import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { isEmpty, isNil, isUndefined } from "lodash"; import MetadataPanel from "../shared/MetadataPanel"; import T2Table from "../shared/T2Table"; -import { useDispatch, useSelector } from "react-redux"; -import { searchIssue } from "../../actions/fileops.actions"; import ellipsize from "ellipsize"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import axios from "axios"; /** * Component that tabulates the contents of the user's ThreeTwo Library. @@ -16,30 +16,36 @@ import ellipsize from "ellipsize"; * */ export const Library = (): ReactElement => { - const searchResults = useSelector( - (state: RootState) => state.fileOps.libraryComics, - ); - const searchError = useSelector((state: RootState) => state.fileOps.librarySearchError); - const dispatch = useDispatch(); - useEffect(() => { - dispatch( - searchIssue( - { - query: {}, - }, - { - pagination: { - size: 15, - from: 0, - }, - type: "all", - trigger: "libraryPage", - }, - ), - ); - }, []); + // Default page state + // offset: 0 + const [offset, setOffset] = useState(0); - // programatically navigate to comic detail + // Method to fetch paginated issues + const fetchIssues = async (searchQuery, offset, type) => { + let pagination = { + size: 15, + from: offset, + }; + return await axios({ + method: "POST", + url: "http://localhost:3000/api/search/searchIssue", + data: { + searchQuery, + pagination, + type, + }, + }); + }; + + const { data, isLoading, isError, isPlaceholderData } = useQuery({ + queryKey: ["comics", offset], + queryFn: () => fetchIssues({}, offset, "all"), + placeholderData: keepPreviousData, + }); + + const searchResults = data?.data; + + // Programmatically navigate to comic detail const navigate = useNavigate(); const navigateToComicDetail = (row) => { navigate(`/comic/details/${row.original._id}`); @@ -156,23 +162,11 @@ export const Library = (): ReactElement => { * @returns void * **/ - const nextPage = useCallback((pageIndex: number, pageSize: number) => { - dispatch( - searchIssue( - { - query: {}, - }, - { - pagination: { - size: pageSize, - from: pageSize * pageIndex + 1, - }, - type: "all", - trigger: "libraryPage", - }, - ), - ); - }, []); + const nextPage = (pageIndex: number, pageSize: number) => { + if (!isPlaceholderData) { + setOffset(pageSize * pageIndex + 1); + } + }; /** * Pagination control that fetches the previous x (pageSize) items @@ -181,29 +175,15 @@ export const Library = (): ReactElement => { * @param {number} pageSize * @returns void **/ - const previousPage = useCallback((pageIndex: number, pageSize: number) => { + const previousPage = (pageIndex: number, pageSize: number) => { let from = 0; if (pageIndex === 2) { - from = (pageIndex - 1) * pageSize + 2 - 17; + from = (pageIndex - 1) * pageSize + 2 - (pageSize + 2); } else { - from = (pageIndex - 1) * pageSize + 2 - 16; + from = (pageIndex - 1) * pageSize + 2 - (pageSize + 1); } - dispatch( - searchIssue( - { - query: {}, - }, - { - pagination: { - size: pageSize, - from, - }, - type: "all", - trigger: "libraryPage", - }, - ), - ); - }, []); + setOffset(from); + }; // ImportStatus.propTypes = { // value: PropTypes.bool.isRequired, @@ -214,13 +194,13 @@ export const Library = (): ReactElement => {

Library

- {!isEmpty(searchResults) ? ( + {!isUndefined(searchResults?.hits) ? (
{ back.
-
-                {!isUndefined(searchError.data) &&
-                  JSON.stringify(
-                    searchError.data.meta.body.error.root_cause,
+              {!isUndefined(searchResults?.data?.meta?.body) ? (
+                
+                  {JSON.stringify(
+                    searchResults.data.meta.body.error.root_cause,
                     null,
                     4,
                   )}
-              
+
+ ) : null}
)} diff --git a/src/client/components/Library/SearchBar.tsx b/src/client/components/Library/SearchBar.tsx index 089704c..2e3d774 100644 --- a/src/client/components/Library/SearchBar.tsx +++ b/src/client/components/Library/SearchBar.tsx @@ -2,29 +2,27 @@ import React, { ReactElement, useCallback } from "react"; import PropTypes from "prop-types"; import { Form, Field } from "react-final-form"; import { Link } from "react-router-dom"; -import { useDispatch } from "react-redux"; import { searchIssue } from "../../actions/fileops.actions"; export const SearchBar = (): ReactElement => { - const dispatch = useDispatch(); const handleSubmit = useCallback((e) => { - dispatch( - searchIssue( - { - query: { - volumeName: e.search, - }, - }, - { - pagination: { - size: 25, - from: 0, - }, - type: "volumeName", - trigger: "libraryPage", - }, - ), - ); + // dispatch( + // searchIssue( + // { + // query: { + // volumeName: e.search, + // }, + // }, + // { + // pagination: { + // size: 25, + // from: 0, + // }, + // type: "volumeName", + // trigger: "libraryPage", + // }, + // ), + // ); }, []); return (
@@ -56,7 +54,6 @@ export const SearchBar = (): ReactElement => { )} /> -
); }; diff --git a/src/client/components/PullList/PullList.tsx b/src/client/components/PullList/PullList.tsx index b264248..8b2cc83 100644 --- a/src/client/components/PullList/PullList.tsx +++ b/src/client/components/PullList/PullList.tsx @@ -1,25 +1,23 @@ 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 "../shared/Carda"; import ellipsize from "ellipsize"; import { isNil } from "lodash"; export const PullList = (): ReactElement => { - const pullListComics = useSelector( - (state: RootState) => state.comicInfo.pullList, - ); + // const pullListComics = useSelector( + // (state: RootState) => state.comicInfo.pullList, + // ); - const dispatch = useDispatch(); useEffect(() => { - dispatch( - getWeeklyPullList({ - startDate: "2023-7-28", - pageSize: "15", - currentPage: "1", - }), - ); + // dispatch( + // getWeeklyPullList({ + // startDate: "2023-7-28", + // pageSize: "15", + // currentPage: "1", + // }), + // ); }, []); const nextPageHandler = () => {}; const previousPageHandler = () => {}; diff --git a/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx index c2a88bf..845d2a8 100644 --- a/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx +++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx @@ -1,39 +1,68 @@ import React, { ReactElement, useEffect, useState, useContext } from "react"; 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 { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useStore } from "../../../store"; +import axios from "axios"; -export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => { - const dispatch = useDispatch(); - const [hubList, setHubList] = useState([]); - const airDCPPConfiguration = useContext(AirDCPPSocketContext); +export const AirDCPPHubsForm = (): ReactElement => { + const queryClient = useQueryClient(); const { - airDCPPState: { settings, socket }, - } = airDCPPConfiguration; + airDCPPSocketInstance, + airDCPPClientConfiguration, + airDCPPSessionInformation, + } = useStore((state) => ({ + airDCPPSocketInstance: state.airDCPPSocketInstance, + airDCPPClientConfiguration: state.airDCPPClientConfiguration, + airDCPPSessionInformation: state.airDCPPSessionInformation, + })); - useEffect(() => { - (async () => { - if (!isEmpty(settings)) { - const hubs = await socket.get(`hubs`); - const hubSelectionOptions = hubs.map(({ hub_url, identity }) => ({ - value: hub_url, - label: identity.name, - })); - - setHubList(hubSelectionOptions); - } - })(); - }, []); - - const onSubmit = (values) => { - if (!isUndefined(values.hubs)) { - dispatch(saveSettings({ ...settings, hubs: values.hubs }, settings._id)); - } - }; + const { + data: settings, + isLoading, + isError, + } = useQuery({ + queryKey: ["settings"], + queryFn: async () => + await axios({ + url: "http://localhost:3000/api/settings/getAllSettings", + method: "GET", + }), + }); + /** + * Get the hubs list from an AirDCPP Socket + */ + const { data: hubs } = useQuery({ + queryKey: [], + queryFn: async () => await airDCPPSocketInstance.get(`hubs`), + enabled: !!settings, + }); + let hubList = {}; + if (!isEmpty(hubs)) { + console.log("hs", hubs); + hubList = hubs.map(({ hub_url, identity }) => ({ + value: hub_url, + label: identity.name, + })); + } + console.log(hubList); + const { mutate } = useMutation({ + mutationFn: async (values) => + await axios({ + url: `http://localhost:3000/api/settings/saveSettings`, + method: "POST", + data: { + settingsPayload: values, + settingsObjectId: settings?.data._id, + settingsKey: "directConnect", + }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["settings"] }); + }, + }); const validate = async () => {}; const SelectAdapter = ({ input, ...rest }) => { @@ -42,53 +71,70 @@ export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => { return ( <> -
( - -
-

Hubs

-
- Select the hubs you want to perform searches against. -
-
-
- -
- + {!isEmpty(hubList) && !isUndefined(hubs) ? ( + ( + +
+

Hubs

+
+ Select the hubs you want to perform searches against. +
+
+
+ +
+ +
-
- - - )} - /> -
-
-
- Your selection in the dropdown will replace the - existing selection. + + + )} + /> + ) : ( + <> +
+
+ No configured hubs detected in AirDC++.
+ Configure to a hub in AirDC++ and then select a default hub here. +
+
+ + )} + {!isEmpty(settings?.data.directConnect?.client.hubs) ? ( + <> +
+
+
+ Your selection in the dropdown will replace the + existing selection. +
+
-
-
-
-
Selected hubs
- {settings.directConnect.client.hubs.map(({ value, label }) => ( -
-
{label}
- {value} +
+
Default Hub For Searches:
+ {settings?.data.directConnect?.client.hubs.map( + ({ value, label }) => ( +
+
{label}
+ {value} +
+ ), + )}
- ))} -
+ + ) : null} ); }; diff --git a/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx index 4f97895..c8e63d2 100644 --- a/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx +++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx @@ -2,60 +2,95 @@ 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 { initializeAirDCPPSocket, useStore } from "../../../store/index"; import { useShallow } from "zustand/react/shallow"; +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; 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 { setState } = useStore; const { airDCPPSocketConnected, airDCPPDisconnectionInfo, - airDCPPSocketConnectionInformation, + airDCPPSessionInformation, airDCPPClientConfiguration, + airDCPPSocketInstance, + setAirDCPPSocketInstance, } = useStore( useShallow((state) => ({ airDCPPSocketConnected: state.airDCPPSocketConnected, airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo, airDCPPClientConfiguration: state.airDCPPClientConfiguration, - airDCPPSocketConnectionInformation: - state.airDCPPSocketConnectionInformation, + airDCPPSessionInformation: state.airDCPPSessionInformation, + airDCPPSocketInstance: state.airDCPPSocketInstance, + setAirDCPPSocketInstance: state.setAirDCPPSocketInstance, })), ); - const onSubmit = useCallback(async (values) => { - try { - // airDCPPSettings.setSettings(values); - } catch (error) { - console.log(error); - } - }, []); - const removeSettings = useCallback(async () => { - // airDCPPSettings.setSettings({}); - }, []); + /** + * Mutation to update settings and subsequently initialize + * AirDC++ socket with those settings + */ + const { mutate } = useMutation({ + mutationFn: async (values) => + await axios({ + url: `http://localhost:3000/api/settings/saveSettings`, + method: "POST", + data: { settingsPayload: values, settingsKey: "directConnect" }, + }), + onSuccess: async (values) => { + const { + data: { + directConnect: { + client: { host }, + }, + }, + } = values; + const dcppSocketInstance = await initializeAirDCPPSocket(host); + setState({ + airDCPPClientConfiguration: host, + airDCPPSocketInstance: dcppSocketInstance, + }); + }, + }); + const deleteSettingsMutation = useMutation( + async () => + await axios.post("http://localhost:3000/api/settings/saveSettings", { + settingsPayload: {}, + settingsKey: "directConnect", + }), + ); + + // const removeSettings = useCallback(async () => { + // // airDCPPSettings.setSettings({}); + // }, []); // const initFormData = !isUndefined(airDCPPClientConfiguration) ? airDCPPClientConfiguration : {}; - + console.log(airDCPPClientConfiguration); return ( <> - {!isEmpty(airDCPPSocketConnectionInformation) ? ( - + {!isEmpty(airDCPPSessionInformation) ? ( + ) : null} {!isEmpty(airDCPPClientConfiguration) ? (

-

diff --git a/src/client/components/Settings/Settings.tsx b/src/client/components/Settings/Settings.tsx index 06c0f85..d6d19d0 100644 --- a/src/client/components/Settings/Settings.tsx +++ b/src/client/components/Settings/Settings.tsx @@ -14,7 +14,11 @@ export const Settings = (props: ISettingsProps): ReactElement => { const settingsContent = [ { id: "adc-hubs", - content:
{/* */}
, + content: ( +
+ +
+ ), }, { id: "adc-connection", @@ -38,7 +42,11 @@ export const Settings = (props: ISettingsProps): ReactElement => { }, { id: "flushdb", - content:
{/* */}
, + content: ( +
+ +
+ ), }, ]; return ( diff --git a/src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx b/src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx index d19c9e5..68e41aa 100644 --- a/src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx +++ b/src/client/components/Settings/SystemSettings/SystemSettingsForm.tsx @@ -1,15 +1,16 @@ -import React, { ReactElement, useCallback } from "react"; -import { flushDb } from "../../../actions/settings.actions"; -import { useDispatch, useSelector } from "react-redux"; +import React, { ReactElement } from "react"; +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; export const SystemSettingsForm = (): ReactElement => { - const dispatch = useDispatch(); - const isSettingsCallInProgress = useSelector( - (state: RootState) => state.settings.inProgress, - ); - const flushDatabase = useCallback(() => { - dispatch(flushDb()); - }, []); + const { mutate: flushDb, isLoading } = useMutation({ + mutationFn: async () => { + await axios({ + url: `http://localhost:3000/api/library/flushDb`, + method: "POST", + }); + }, + }); return (
@@ -48,11 +49,9 @@ export const SystemSettingsForm = (): ReactElement => {
diff --git a/src/client/components/shared/ErrorPage.tsx b/src/client/components/shared/ErrorPage.tsx new file mode 100644 index 0000000..df44293 --- /dev/null +++ b/src/client/components/shared/ErrorPage.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const ErrorPage = () => { + return <>Error has been encountered.; +}; diff --git a/src/client/components/shared/Navbar.tsx b/src/client/components/shared/Navbar.tsx index f512a61..1fdeeed 100644 --- a/src/client/components/shared/Navbar.tsx +++ b/src/client/components/shared/Navbar.tsx @@ -1,15 +1,46 @@ -import React, { useContext } from "react"; +import React from "react"; import { SearchBar } from "../GlobalSearchBar/SearchBar"; import { DownloadProgressTick } from "../ComicDetail/DownloadProgressTick"; import { Link } from "react-router-dom"; -import { isUndefined } from "lodash"; +import { isEmpty, isNil, isUndefined } from "lodash"; import { format, fromUnixTime } from "date-fns"; +import { useStore } from "../../store/index"; +import { useShallow } from "zustand/react/shallow"; const Navbar: React.FunctionComponent = (props) => { + const { + airDCPPSocketConnected, + airDCPPDisconnectionInfo, + airDCPPSessionInformation, + airDCPPDownloadTick, + importJobQueue, + } = useStore( + useShallow((state) => ({ + airDCPPSocketConnected: state.airDCPPSocketConnected, + airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo, + airDCPPSessionInformation: state.airDCPPSessionInformation, + airDCPPDownloadTick: state.airDCPPDownloadTick, + importJobQueue: state.importJobQueue, + })), + ); + // 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, + // ); + return (