Compare commits

...

33 Commits

Author SHA1 Message Date
90690c6876 Re-added yarn.lock 2023-11-07 11:44:07 -06:00
08ff21a987 AirDCPP code cleaned up from App.tsx 2023-11-07 11:44:04 -06:00
07724380f7 🧸 Zustand-ified AirDCPP Form 2023-11-07 11:44:04 -06:00
5b27658dc3 🐛 Debugging AirDCPP Settings Form page 2023-11-07 11:44:04 -06:00
91d0110de2 🔧 Fixed early init error on the store 2023-11-07 11:44:04 -06:00
730141ff26 🔧 Removing context from AirDCPP settings page 2023-11-07 11:44:04 -06:00
59801763e6 🔧 Removed context 2023-11-07 11:44:02 -06:00
7639833757 🏗️ Refactoring AirDCPP Settings Form with react-query 2023-11-07 11:43:56 -06:00
a0f7280fbb 🏗️ Refactored AirDCPP context using react-query 2023-11-07 11:43:56 -06:00
1f8dded15e 🏗 Added error check state 2023-11-07 11:43:56 -06:00
c3b9ad9d91 🏗️ Added loading state check 2023-11-07 11:43:56 -06:00
e89a7f9c91 📝 qbittorrent form RU actions first draft 2023-11-07 11:43:56 -06:00
2a6eeaf943 🧩 Added react-query query to qBittorrentSettings page 2023-11-07 11:43:56 -06:00
d45c53dff9 🏗️ Trying out react-query 2023-11-07 11:43:56 -06:00
2df0fce792 ⬇️ Fetched qBittorrent settings 2023-11-07 11:43:54 -06:00
4ea9086e3f Migrating from Redux to RTK-query 2023-11-07 11:43:54 -06:00
206c2eeb4b 🏗️ Reorganized tabs 2023-11-07 11:43:51 -06:00
6cd6612ce1 📝 Abstracted connection form into a component 2023-11-07 11:43:51 -06:00
538ca61fa8 📝 Purified Card Component 2023-11-07 11:43:51 -06:00
69cebe82dd 🔧 Added Prowlarr settings items in JSON 2023-11-07 11:43:51 -06:00
c6e55a90ce 🔧 Formatting and more layout changes 2023-11-07 11:43:51 -06:00
3ca15148f5 🏗️ Adding fields to the settings form 2023-11-07 11:43:51 -06:00
bd35223f0f 🧑🏼‍🔧 Fixed the hostname regex 2023-11-07 11:43:51 -06:00
4e82bc73e1 ⬆️ Bumped @dnd-kit deps 2023-11-07 11:43:51 -06:00
ac80c23fa8 📝 Qbittorrent form scaffold 2023-11-07 11:43:47 -06:00
f9f3109132 🔧 Tooling changes 2023-11-07 11:43:33 -06:00
111962acd1 🔧 More path fixes 2023-11-07 11:43:10 -06:00
502ed707ab 🔧 Cleaned up Search and Import component hierarchy 2023-11-07 11:43:10 -06:00
0058df3859 🔧 Fixed broken paths 2023-11-07 11:43:10 -06:00
8c4027f383 🔧 Cleaned up folder structure 2023-11-07 11:43:10 -06:00
9fe69d21db 🔧 Some refactoring 2023-11-07 11:43:10 -06:00
c591f1db2f 🔧 Added scaffold for the qBittorrent connection form 2023-11-07 11:43:09 -06:00
093dcc448c 🌊 qBittorrent settings scaffold 2023-11-07 11:43:09 -06:00
52 changed files with 2715 additions and 5428 deletions

View File

@@ -7,7 +7,6 @@ const config: StorybookConfig = {
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-onboarding", "@storybook/addon-onboarding",
"@storybook/addon-interactions", "@storybook/addon-interactions",
"@storybook/addon-mdx-gfm"
], ],
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",

View File

@@ -1,34 +1,25 @@
{ {
"tags": { "tags": {
"allowUnknownTags": true, "allowUnknownTags": false
"dictionaries": [ },
"jsdoc", "source": {
"closure" "include": [
] "./src/client"
}, ],
"source": { "includePattern": "\\.(jsx|js|ts|tsx)$"
"include": [ },
"./src/client" "plugins": [
"plugins/markdown"
], ],
"includePattern": "\\.(jsx|js|ts|tsx)$" "opts": {
}, "template": "node_modules/tui-jsdoc-template",
"plugins": [ "encoding": "utf8",
"better-docs/component", "destination": "docs/",
"better-docs/category", "recurse": true,
"plugins/markdown", "verbose": true
"node_modules/better-docs/typescript" },
], "templates": {
"templates": { "cleverLinks": false,
"better-docs": { "monospaceLinks": false
"name": "ThreeTwo UI components"
} }
},
"opts": {
"destination": "docs/",
"readme": "README.md",
"recurse": true,
"encoding": "utf8",
"verbose": true,
"template": "node_modules/better-docs"
}
} }

View File

@@ -15,13 +15,13 @@
"author": "Rishi Ghan", "author": "Rishi Ghan",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/core": "^4.0.0", "@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/sortable": "^5.0.0", "@dnd-kit/utilities": "^3.2.1",
"@dnd-kit/utilities": "^3.2.0",
"@fortawesome/fontawesome-free": "^6.3.0", "@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", "@rollup/plugin-node-resolve": "^15.0.1",
"@tanstack/react-query": "^5.0.5",
"@tanstack/react-table": "^8.9.3", "@tanstack/react-table": "^8.9.3",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
@@ -30,9 +30,7 @@
"axios": "^1.3.4", "axios": "^1.3.4",
"axios-cache-interceptor": "^1.0.1", "axios-cache-interceptor": "^1.0.1",
"axios-rate-limit": "^1.3.0", "axios-rate-limit": "^1.3.0",
"babel-polyfill": "^6.26.0", "babel-plugin-styled-components": "^2.1.4",
"babel-preset-minify": "^0.5.2",
"better-docs": "^2.7.2",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"ellipsize": "^0.5.1", "ellipsize": "^0.5.1",
@@ -44,7 +42,6 @@
"html-to-text": "^8.1.0", "html-to-text": "^8.1.0",
"jsdoc": "^3.6.10", "jsdoc": "^3.6.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"node-sass": "npm:sass",
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"qs": "^6.10.5", "qs": "^6.10.5",
@@ -59,7 +56,6 @@
"react-loader-spinner": "^4.0.0", "react-loader-spinner": "^4.0.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-redux": "^8.0.5",
"react-router": "^6.9.0", "react-router": "^6.9.0",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"react-select": "^5.3.2", "react-select": "^5.3.2",
@@ -69,26 +65,26 @@
"react-stickynode": "^4.1.0", "react-stickynode": "^4.1.0",
"react-textarea-autosize": "^8.3.4", "react-textarea-autosize": "^8.3.4",
"reapop": "^4.2.1", "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", "slick-carousel": "^1.8.1",
"socket.io-client": "^4.3.2", "socket.io-client": "^4.3.2",
"styled-components": "^6.0.7", "styled-components": "^6.0.7",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
"vite": "^4.5.0",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"websocket": "^1.0.34" "websocket": "^1.0.34",
"zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.3.2", "@storybook/addon-essentials": "^7.4.1",
"@storybook/addon-interactions": "^7.3.2", "@storybook/addon-interactions": "^7.4.1",
"@storybook/addon-links": "^7.3.2", "@storybook/addon-links": "^7.4.1",
"@storybook/addon-mdx-gfm": "^7.3.2",
"@storybook/addon-onboarding": "^1.0.8", "@storybook/addon-onboarding": "^1.0.8",
"@storybook/blocks": "^7.3.2", "@storybook/blocks": "^7.4.1",
"@storybook/react": "^7.3.2", "@storybook/react": "^7.4.1",
"@storybook/react-vite": "^7.3.2", "@storybook/react-vite": "^7.4.1",
"@storybook/testing-library": "^0.2.0", "@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", "@tsconfig/node14": "^1.0.0",
"@types/ellipsize": "^0.1.1", "@types/ellipsize": "^0.1.1",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",
@@ -98,17 +94,14 @@
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-redux": "^7.1.25", "@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", "body-parser": "^1.19.0",
"bulma": "^0.9.4", "bulma": "^0.9.4",
"eslint": "^7.22.0", "docdash": "^2.0.2",
"eslint-config-airbnb": "^18.2.1", "eslint": "^8.49.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-css-modules": "^2.11.0", "eslint-plugin-css-modules": "^2.11.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^46.6.0",
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.22.0",
@@ -117,15 +110,15 @@
"install": "^0.13.0", "install": "^0.13.0",
"jest": "^29.6.3", "jest": "^29.6.3",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"npm": "^8.11.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"rimraf": "^4.1.3", "rimraf": "^4.1.3",
"sass": "^1.66.1", "sass": "^1.66.1",
"storybook": "^7.3.2", "storybook": "^7.3.2",
"tslint": "^6.1.3", "tui-jsdoc-template": "^1.2.2",
"typescript": "^5.1.6", "typescript": "^5.1.6"
"vite": "^4.4.9" },
"resolutions": {
"styled-components": "^5"
} }
} }

View File

@@ -3,25 +3,14 @@ import {
SETTINGS_OBJECT_FETCHED, SETTINGS_OBJECT_FETCHED,
SETTINGS_CALL_IN_PROGRESS, SETTINGS_CALL_IN_PROGRESS,
SETTINGS_DB_FLUSH_SUCCESS, SETTINGS_DB_FLUSH_SUCCESS,
} from "../constants/action-types"; SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
} from "../reducers/settings.reducer";
import { import {
LIBRARY_SERVICE_BASE_URI, LIBRARY_SERVICE_BASE_URI,
SETTINGS_SERVICE_BASE_URI, SETTINGS_SERVICE_BASE_URI,
QBITTORRENT_SERVICE_BASE_URI,
} from "../constants/endpoints"; } 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) => { export const getSettings = (settingsKey?) => async (dispatch) => {
const result = await axios({ const result = await axios({
url: `${SETTINGS_SERVICE_BASE_URI}/getSettings`, 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) => {};

View File

@@ -1,21 +1,18 @@
import React, { ReactElement, useContext, useEffect } from "react"; import React, { ReactElement, useContext, useEffect } from "react";
import Dashboard from "./Dashboard/Dashboard"; import Dashboard from "./Dashboard/Dashboard";
import Import from "./Import"; import Import from "./Import/Import";
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer"; import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
import TabulatedContentContainer from "./Library/TabulatedContentContainer"; import TabulatedContentContainer from "./Library/TabulatedContentContainer";
import LibraryGrid from "./Library/LibraryGrid"; import LibraryGrid from "./Library/LibraryGrid";
import Search from "./Search"; import Search from "./Search/Search";
import Settings from "./Settings"; import Settings from "./Settings/Settings";
import VolumeDetail from "./VolumeDetail/VolumeDetail"; import VolumeDetail from "./VolumeDetail/VolumeDetail";
import Downloads from "./Downloads/Downloads"; import Downloads from "./Downloads/Downloads";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import Navbar from "./Navbar"; import Navbar from "./shared/Navbar";
import "../assets/scss/App.scss"; import "../assets/scss/App.scss";
import {
AirDCPPSocketContextProvider,
AirDCPPSocketContext,
} from "../context/AirDCPPSocket";
import { SocketIOProvider } from "../context/SocketIOContext"; import { SocketIOProvider } from "../context/SocketIOContext";
import socketIOConnectionInstance from "../shared/socket.io/instance"; import socketIOConnectionInstance from "../shared/socket.io/instance";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
@@ -23,7 +20,6 @@ import {
AIRDCPP_DOWNLOAD_PROGRESS_TICK, AIRDCPP_DOWNLOAD_PROGRESS_TICK,
LS_SINGLE_IMPORT, LS_SINGLE_IMPORT,
} from "../constants/action-types"; } from "../constants/action-types";
import { useDispatch, useSelector } from "react-redux";
/** /**
* Method that initializes an AirDC++ socket connection * 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 * 2. Handles errors in case the connection to AirDC++ is not established or terminated
* @returns void * @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 => { export const App = (): ReactElement => {
const dispatch = useDispatch(); // useEffect(() => {
useEffect(() => { // // Check if there is a sessionId in localStorage
// Check if there is a sessionId in localStorage // const sessionId = localStorage.getItem("sessionId");
const sessionId = localStorage.getItem("sessionId"); // if (!isNil(sessionId)) {
if (!isNil(sessionId)) { // // Resume the session
// Resume the session // dispatch({
dispatch({ // type: "RESUME_SESSION",
type: "RESUME_SESSION", // meta: { remote: true },
meta: { remote: true }, // session: { sessionId },
session: { sessionId }, // });
}); // } else {
} else { // // Inititalize the session and persist the sessionId to localStorage
// Inititalize the session and persist the sessionId to localStorage // socketIOConnectionInstance.on("sessionInitialized", (sessionId) => {
socketIOConnectionInstance.on("sessionInitialized", (sessionId) => { // localStorage.setItem("sessionId", sessionId);
localStorage.setItem("sessionId", sessionId); // });
}); // }
} // }, []);
}, []); return <>{/* The rest of your application */}</>;
return (
<SocketIOProvider socket={socketIOConnectionInstance}>
<AirDCPPSocketContextProvider>
<div>
<AirDCPPSocketComponent />
<Navbar />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} />
<Route
path="/library"
element={<TabulatedContentContainer category="library" />}
/>
<Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} />
<Route
path={"/comic/details/:comicObjectId"}
element={<ComicDetailContainer />}
/>
<Route
path={"/volume/details/:comicObjectId"}
element={<VolumeDetail />}
/>
<Route path="/settings" element={<Settings />} />
<Route
path="/pull-list/all"
element={<TabulatedContentContainer category="pullList" />}
/>
<Route
path="/wanted/all"
element={<TabulatedContentContainer category="wanted" />}
/>
<Route
path="/volumes/all"
element={<TabulatedContentContainer category="volumes" />}
/>
</Routes>
</div>
</AirDCPPSocketContextProvider>
</SocketIOProvider>
);
}; };
export default App; export default App;

View File

@@ -1,11 +1,11 @@
import React, { useState, ReactElement, useCallback } from "react"; import React, { useState, ReactElement, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Card from "../Carda"; import Card from "../shared/Carda";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { RawFileDetails } from "./RawFileDetails"; import { RawFileDetails } from "./RawFileDetails";
import { ComicVineSearchForm } from "../ComicVineSearchForm"; import { ComicVineSearchForm } from "./ComicVineSearchForm";
import TabControls from "./TabControls"; import TabControls from "./TabControls";
import { EditMetadataPanel } from "./EditMetadataPanel"; import { EditMetadataPanel } from "./EditMetadataPanel";
@@ -198,8 +198,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
}, },
{ {
id: 4, id: 4,
icon: <i className="fa-solid fa-floppy-disk"></i>, icon: <i className="fa-solid fa-circle-nodes"></i>,
name: "Acquisition", name: "DC++ Search",
content: ( content: (
<AcquisitionPanel <AcquisitionPanel
query={airDCPPQuery} query={airDCPPQuery}
@@ -213,6 +213,13 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
}, },
{ {
id: 5, id: 5,
icon: <i className="fa-solid fa-droplet"></i>,
name: "Torrent Search",
content: <>Torrents</>,
shouldShow: true,
},
{
id: 6,
icon: null, icon: null,
name: !isEmpty(data.data) ? ( name: !isEmpty(data.data) ? (
<span className="download-tab-name">Downloads</span> <span className="download-tab-name">Downloads</span>
@@ -290,7 +297,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<ComicViewer <ComicViewer
pages={extractedComicBook} pages={extractedComicBook}
direction="ltr" direction="ltr"
className={{closeButton: "border: 1px solid red;"}} className={{
closeButton: "border: 1px solid red;",
}}
/> />
)} )}
</Modal> </Modal>
@@ -317,4 +326,4 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
); );
}; };
export default ComicDetail; export default ComicDetail;

View File

@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import Card from "../Carda"; import Card from "../shared/Carda";
export const ComicVineDetails = (props): ReactElement => { export const ComicVineDetails = (props): ReactElement => {
const { data, updatedAt } = props; const { data, updatedAt } = props;
return ( return (

View File

@@ -1,6 +1,6 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { ComicVineSearchForm } from "../ComicVineSearchForm"; import { ComicVineSearchForm } from "../ComicVineSearchForm";
import MatchResult from "../MatchResult"; import MatchResult from "./MatchResult";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
export const ComicVineMatchPanel = (comicVineData): ReactElement => { export const ComicVineMatchPanel = (comicVineData): ReactElement => {

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import Collapsible from "react-collapsible"; import Collapsible from "react-collapsible";
import { fetchComicVineMatches } from "../actions/fileops.actions"; import { fetchComicVineMatches } from "../../actions/fileops.actions";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
/** /**

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { isNil, map } from "lodash"; import { isNil, map } from "lodash";
import { applyComicVineMatch } from "../actions/comicinfo.actions"; import { applyComicVineMatch } from "../../actions/comicinfo.actions";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";

View File

@@ -25,12 +25,15 @@ export const TabControls = (props): ReactElement => {
> >
{/* Downloads tab and count badge */} {/* Downloads tab and count badge */}
<a> <a>
{id === 5 && {id === 6 &&
!isNil(comicBookDetailData.acquisition.directconnect) ? ( !isNil(comicBookDetailData.acquisition.directconnect) ? (
<span className="download-icon-labels"> <span className="download-icon-labels">
<i className="fa-solid fa-download"></i> <i className="fa-solid fa-download"></i>
<span className="tag downloads-count is-info is-light"> <span className="tag downloads-count is-info is-light">
{comicBookDetailData.acquisition.directconnect.downloads.length} {
comicBookDetailData.acquisition.directconnect.downloads
.length
}
</span> </span>
</span> </span>
) : ( ) : (

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useCallback, useState } from "react"; import React, { ReactElement, useCallback, useState } from "react";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { DnD } from "../../DnD"; import { DnD } from "../../shared/Draggable/DnD";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import Sticky from "react-stickynode"; import Sticky from "react-stickynode";
import SlidingPane from "react-sliding-pane"; import SlidingPane from "react-sliding-pane";

View File

@@ -12,114 +12,53 @@ import {
} from "../../actions/fileops.actions"; } from "../../actions/fileops.actions";
import { getLibraryStatistics } from "../../actions/comicinfo.actions"; import { getLibraryStatistics } from "../../actions/comicinfo.actions";
import { isEmpty, isNil } from "lodash"; import { isEmpty, isNil } from "lodash";
import Header from "../Header"; import Header from "../shared/Header";
export const Dashboard = (): ReactElement => { export const Dashboard = (): ReactElement => {
const dispatch = useDispatch(); // useEffect(() => {
useEffect(() => { // dispatch(fetchVolumeGroups());
dispatch(fetchVolumeGroups()); // dispatch(
dispatch( // getComicBooks({
getComicBooks({ // paginationOptions: {
paginationOptions: { // page: 0,
page: 0, // limit: 5,
limit: 5, // sort: { updatedAt: "-1" },
sort: { updatedAt: "-1" }, // },
}, // predicate: { "acquisition.source.wanted": false },
predicate: { "acquisition.source.wanted": false }, // comicStatus: "recent",
comicStatus: "recent", // }),
}), // );
); // dispatch(
dispatch( // getComicBooks({
getComicBooks({ // paginationOptions: {
paginationOptions: { // page: 0,
page: 0, // limit: 5,
limit: 5, // sort: { updatedAt: "-1" },
sort: { updatedAt: "-1" }, // },
}, // predicate: { "acquisition.source.wanted": true },
predicate: { "acquisition.source.wanted": true }, // comicStatus: "wanted",
comicStatus: "wanted", // }),
}), // );
); // dispatch(getLibraryStatistics());
dispatch(getLibraryStatistics()); // }, []);
}, []); //
// const recentComics = useSelector(
const recentComics = useSelector( // (state: RootState) => state.fileOps.recentComics,
(state: RootState) => state.fileOps.recentComics, // );
); // const wantedComics = useSelector(
const wantedComics = useSelector( // (state: RootState) => state.fileOps.wantedComics,
(state: RootState) => state.fileOps.wantedComics, // );
); // const volumeGroups = useSelector(
const volumeGroups = useSelector( // (state: RootState) => state.fileOps.comicVolumeGroups,
(state: RootState) => state.fileOps.comicVolumeGroups, // );
); //
// const libraryStatistics = useSelector(
const libraryStatistics = useSelector( // (state: RootState) => state.comicInfo.libraryStatistics,
(state: RootState) => state.comicInfo.libraryStatistics, // );
);
return ( return (
<div className="container"> <div className="container">
<section className="section"> <section className="section">
<h1 className="title">Dashboard</h1> <h1 className="title">Dashboard</h1>
{!isEmpty(recentComics) ? (
<>
{/* Pull List */}
<PullList issues={recentComics} />
<>
<div className="content mt-6">
<Header
headerContent="Import Activity"
subHeaderContent="Results aggregated from the last import"
iconClassNames="fa-solid fa-file-invoice mr-2"
/>
</div>
<table className="table">
<thead>
<tr>
<th>
<abbr title="Position">Pos</abbr>
</th>
<th>Team</th>
<th>
<abbr title="Played">Pld</abbr>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td>38</td>
</tr>
</tbody>
</table>
</>
{/* Stats */}
{!isEmpty(libraryStatistics) && (
<LibraryStatistics stats={libraryStatistics} />
)}
{/* Wanted comics */}
{!isEmpty(wantedComics) && (
<WantedComicsList comics={wantedComics} />
)}
{/* Recent imports */}
<RecentlyImported comicBookCovers={recentComics} />
{/* Volumes */}
{!isEmpty(volumeGroups) && (
<VolumeGroups volumeGroups={volumeGroups} />
)}
</>
) : (
<ZeroState
header={"Set the source directory"}
message={
"No comics were found! Please point ThreeTwo! to a directory..."
}
/>
)}
</section> </section>
</div> </div>
); );

View File

@@ -1,7 +1,7 @@
import { isNil, map } from "lodash"; import { isNil, map } from "lodash";
import React, { createRef, ReactElement, useCallback, useEffect } from "react"; import React, { createRef, ReactElement, useCallback, useEffect } from "react";
import Card from "../Carda"; import Card from "../shared/Carda";
import Header from "../Header"; import Header from "../shared/Header";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { getWeeklyPullList } from "../../actions/comicinfo.actions"; import { getWeeklyPullList } from "../../actions/comicinfo.actions";
@@ -21,7 +21,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2023-8-9", startDate: "2023-9-9",
pageSize: "15", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),
@@ -91,7 +91,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
<div className="content"> <div className="content">
<Header headerContent="Discover" <Header headerContent="Discover"
subHeaderContent="Pull List aggregated for the week from League Of Comic Geeks" subHeaderContent="Pull List aggregated for the week from League Of Comic Geeks"
iconClassNames="fa-solid fa-splotch mr-2"/> iconClassNames="fa-solid fa-binoculars mr-2"/>
<div className="field is-grouped"> <div className="field is-grouped">
{/* select week */} {/* select week */}
<div className="control"> <div className="control">

View File

@@ -1,5 +1,5 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import Card from "../Carda"; import Card from "../shared/Carda";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map } from "lodash";

View File

@@ -1,5 +1,5 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import Card from "../Carda"; import Card from "../shared/Carda";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map } from "lodash";

View File

@@ -1,7 +1,7 @@
import { debounce, isEmpty, map } from "lodash"; import { debounce, isEmpty, map } from "lodash";
import React, { ReactElement, useCallback, useState } from "react"; import React, { ReactElement, useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import Card from "../Carda"; import Card from "../shared/Carda";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import MetadataPanel from "../shared/MetadataPanel"; import MetadataPanel from "../shared/MetadataPanel";

View File

@@ -4,7 +4,7 @@ import {
fetchComicBookMetadata, fetchComicBookMetadata,
getImportJobResultStatistics, getImportJobResultStatistics,
setQueueControl, setQueueControl,
} from "../actions/fileops.actions"; } from "../../actions/fileops.actions";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import { format } from "date-fns"; import { format } from "date-fns";
import Loader from "react-loader-spinner"; import Loader from "react-loader-spinner";

View File

@@ -12,7 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
import { getComicBooks } from "../../actions/fileops.actions"; import { getComicBooks } from "../../actions/fileops.actions";
import { isNil, isEmpty, isUndefined } from "lodash"; import { isNil, isEmpty, isUndefined } from "lodash";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
import Card from "../Carda"; import Card from "../shared/Carda";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints"; import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";

View File

@@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useMemo } from "react";
import T2Table from "../shared/T2Table"; import T2Table from "../shared/T2Table";
import { getWeeklyPullList } from "../../actions/comicinfo.actions"; import { getWeeklyPullList } from "../../actions/comicinfo.actions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import Card from "../Carda"; import Card from "../shared/Carda";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isNil } from "lodash"; import { isNil } from "lodash";

View File

@@ -1,12 +1,12 @@
import React, { useCallback, ReactElement } from "react"; import React, { useCallback, ReactElement } from "react";
import { isNil, isEmpty } from "lodash"; import { isNil, isEmpty } from "lodash";
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings"; import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
import { importToDB } from "../actions/fileops.actions"; import { importToDB } from "../../actions/fileops.actions";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { comicinfoAPICall } from "../actions/comicinfo.actions"; import { comicinfoAPICall } from "../../actions/comicinfo.actions";
import { search } from "../services/api/SearchApi"; import { search } from "../../services/api/SearchApi";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import Card from "./Carda"; import Card from "../shared/Carda";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import dayjs from "dayjs"; import dayjs from "dayjs";

View File

@@ -3,8 +3,8 @@ import { Form, Field } from "react-final-form";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
import Select from "react-select"; import Select from "react-select";
import { saveSettings } from "../../actions/settings.actions"; import { saveSettings } from "../../../actions/settings.actions";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../../context/AirDCPPSocket";
export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => { export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@@ -2,7 +2,6 @@ import React, { ReactElement } from "react";
export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => { export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
const { settings } = settingsObject; const { settings } = settingsObject;
console.log(settings);
return ( return (
<div className="mt-4 is-clearfix"> <div className="mt-4 is-clearfix">
<div className="card"> <div className="card">

View File

@@ -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 (
<>
<ConnectionForm
initialData={initFormData}
submitHandler={onSubmit}
formHeading={"Configure AirDC++"}
/>
{!isEmpty(airDCPPSocketConnectionInformation) ? (
<AirDCPPSettingsConfirmation
settings={airDCPPSocketConnectionInformation}
/>
) : null}
{!isEmpty(airDCPPClientConfiguration) ? (
<p className="control mt-4">
<button className="button is-danger" onClick={removeSettings}>
Delete
</button>
</p>
) : null}
</>
);
};
export default AirDCPPSettingsForm;

View File

@@ -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 (
<>
<pre>Something went wrong connecting to qBittorrent.</pre>
</>
);
if (!isLoading) {
return (
<>
<ConnectionForm
initialData={hostDetails}
formHeading={"qBittorrent Configuration"}
submitHandler={mutate}
/>
<pre className="mt-5">
{JSON.stringify(qbittorrentClientInfo?.data, null, 4)}
</pre>
</>
);
}
};
export default QbittorrentConnectionForm;

View File

@@ -1,9 +1,10 @@
import React, { useState, ReactElement } from "react"; import React, { useState, ReactElement } from "react";
import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm"; import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm"; import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm";
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm"; import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
import { ServiceStatuses } from "./ServiceStatuses/ServiceStatuses"; import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
import settingsObject from "../constants/settings/settingsMenu.json"; import settingsObject from "../../constants/settings/settingsMenu.json";
import { isUndefined, map } from "lodash"; import { isUndefined, map } from "lodash";
interface ISettingsProps {} interface ISettingsProps {}
@@ -13,7 +14,7 @@ export const Settings = (props: ISettingsProps): ReactElement => {
const settingsContent = [ const settingsContent = [
{ {
id: "adc-hubs", id: "adc-hubs",
content: <div key="adc-hubs">{<AirDCPPHubsForm />}</div>, content: <div key="adc-hubs">{/* <AirDCPPHubsForm /> */}</div>,
}, },
{ {
id: "adc-connection", id: "adc-connection",
@@ -23,17 +24,21 @@ export const Settings = (props: ISettingsProps): ReactElement => {
</div> </div>
), ),
}, },
{
id: "qbt-connection",
content: (
<div key="qbt-connection">
<QbittorrentConnectionForm />
</div>
),
},
{ {
id: "core-service", id: "core-service",
content: <ServiceStatuses />, content: <>a</>,
}, },
{ {
id: "flushdb", id: "flushdb",
content: ( content: <div key="flushdb">{/* <SystemSettingsForm /> */}</div>,
<div key="flushdb">
<SystemSettingsForm />
</div>
),
}, },
]; ];
return ( return (

View File

@@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from "react"; import React, { ReactElement, useCallback } from "react";
import { flushDb } from "../../actions/settings.actions"; import { flushDb } from "../../../actions/settings.actions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
export const SystemSettingsForm = (): ReactElement => { export const SystemSettingsForm = (): ReactElement => {

View File

@@ -2,7 +2,7 @@ import { isArray, map } from "lodash";
import React, { useEffect, ReactElement } from "react"; import React, { useEffect, ReactElement } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { getComicBooksDetailsByIds } from "../../actions/comicinfo.actions"; import { getComicBooksDetailsByIds } from "../../actions/comicinfo.actions";
import { Card } from "../Carda"; import { Card } from "../shared/Carda";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints"; import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";
import { escapePoundSymbol } from "../../shared/utils/formatting.utils"; import { escapePoundSymbol } from "../../shared/utils/formatting.utils";

View File

@@ -9,7 +9,7 @@ import {
} from "../../actions/comicinfo.actions"; } from "../../actions/comicinfo.actions";
import PotentialLibraryMatches from "./PotentialLibraryMatches"; import PotentialLibraryMatches from "./PotentialLibraryMatches";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
import { Card } from "../Carda"; import { Card } from "../shared/Carda";
import SlidingPane from "react-sliding-pane"; import SlidingPane from "react-sliding-pane";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";

View File

@@ -1,7 +1,7 @@
import React, { ReactElement, useEffect, useMemo } from "react"; import React, { ReactElement, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import Card from "../Carda"; import Card from "../shared/Carda";
import T2Table from "../shared/T2Table"; import T2Table from "../shared/T2Table";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { convert } from "html-to-text"; import { convert } from "html-to-text";

View File

@@ -15,7 +15,7 @@ interface ICardProps {
imageStyle?: PropTypes.object; imageStyle?: PropTypes.object;
} }
const renderCard = (props): ReactElement => { const renderCard = (props: ICardProps): ReactElement => {
switch (props.orientation) { switch (props.orientation) {
case "horizontal": case "horizontal":
return ( return (
@@ -85,8 +85,8 @@ const renderCard = (props): ReactElement => {
} }
}; };
export const Card = (props: ICardProps): ReactElement => { export const Card = React.memo(
return renderCard(props); (props: ICardProps): ReactElement => renderCard(props),
}; );
export default Card; export default Card;

View File

@@ -1,62 +1,22 @@
import React, { ReactElement, useCallback, useContext } from "react"; import React, { ReactElement } from "react";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { useDispatch } from "react-redux"; import { hostNameValidator } from "../../../shared/utils/validator.utils";
import { saveSettings, deleteSettings } from "../../actions/settings.actions"; import { isEmpty } from "lodash";
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
: {};
export const ConnectionForm = ({
initialData,
submitHandler,
formHeading,
}): ReactElement => {
return ( return (
<> <>
<Form <Form
onSubmit={onSubmit} onSubmit={submitHandler}
validate={validate} initialValues={initialData}
initialValues={initFormData}
render={({ handleSubmit }) => ( render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<h2>AirDC++ Connection Information</h2> <h2>{formHeading}</h2>
<label className="label">AirDC++ Hostname</label> <label className="label">Hostname</label>
<div className="field has-addons"> <div className="field has-addons">
<p className="control"> <p className="control">
<span className="select"> <span className="select">
@@ -68,13 +28,13 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</span> </span>
</p> </p>
<div className="control is-expanded"> <div className="control is-expanded">
<Field name="hostname" validate={hostValidator}> <Field name="hostname" validate={hostNameValidator}>
{({ input, meta }) => ( {({ input, meta }) => (
<div> <div>
<input <input
{...input} {...input}
type="text" type="text"
placeholder="AirDC++ hostname" placeholder="hostname"
className="input" className="input"
/> />
{meta.error && meta.touched && ( {meta.error && meta.touched && (
@@ -91,14 +51,12 @@ export const AirDCPPSettingsForm = (): ReactElement => {
name="port" name="port"
component="input" component="input"
className="input" className="input"
placeholder="AirDC++ port" placeholder="port"
/> />
</p> </p>
</div> </div>
<div className="field"> <div className="field">
<div className="is-clearfix"> <label className="label">Credentials</label>
<label className="label">Credentials</label>
</div>
<div className="field-body"> <div className="field-body">
<div className="field"> <div className="field">
<p className="control is-expanded has-icons-left"> <p className="control is-expanded has-icons-left">
@@ -125,9 +83,6 @@ export const AirDCPPSettingsForm = (): ReactElement => {
<span className="icon is-small is-left"> <span className="icon is-small is-left">
<i className="fa-solid fa-lock"></i> <i className="fa-solid fa-lock"></i>
</span> </span>
<span className="icon is-small is-right">
<i className="fas fa-check"></i>
</span>
</p> </p>
</div> </div>
</div> </div>
@@ -135,28 +90,19 @@ export const AirDCPPSettingsForm = (): ReactElement => {
<div className="field is-grouped"> <div className="field is-grouped">
<p className="control"> <p className="control">
<button type="submit" className="button is-primary"> <button type="submit" className="button is-primary">
{!isEmpty(initFormData) ? "Update" : "Save"} {!isEmpty(initialData) ? "Update" : "Save"}
</button>
</p>
<p className="control">
<button type="submit" className="button is-danger">
{!isEmpty(initialData) && "Delete"}
</button> </button>
</p> </p>
</div> </div>
</form> </form>
)} )}
/> />
{!isEmpty(airDCPPSettings.airDCPPState.socketConnectionInformation) ? (
<AirDCPPSettingsConfirmation
settings={airDCPPSettings.airDCPPState.socketConnectionInformation}
/>
) : null}
{!isEmpty(airDCPPSettings.airDCPPState.socketConnectionInformation) ? (
<p className="control mt-4">
<button className="button is-danger" onClick={removeSettings}>
Delete
</button>
</p>
) : null}
</> </>
); );
}; };
export default AirDCPPSettingsForm;

View File

@@ -2,7 +2,7 @@ import React, { ReactElement } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { Card } from "../Carda"; import { Card } from "../shared/Carda";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { determineCoverFile } from "../../shared/utils/metadata.utils";
import { find, isUndefined } from "lodash"; import { find, isUndefined } from "lodash";

View File

@@ -1,44 +1,11 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { SearchBar } from "./GlobalSearchBar/SearchBar"; import { SearchBar } from "../GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick"; import { DownloadProgressTick } from "../ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { format, fromUnixTime } from "date-fns"; import { format, fromUnixTime } from "date-fns";
const Navbar: React.FunctionComponent = (props) => { 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 ( return (
<nav className="navbar is-fixed-top"> <nav className="navbar is-fixed-top">
<div className="navbar-brand"> <div className="navbar-brand">
@@ -88,8 +55,6 @@ const Navbar: React.FunctionComponent = (props) => {
Downloads Downloads
</Link> </Link>
<SearchBar />
<Link to="/search" className="navbar-item"> <Link to="/search" className="navbar-item">
Search ComicVine Search ComicVine
</Link> </Link>
@@ -98,102 +63,7 @@ const Navbar: React.FunctionComponent = (props) => {
<div className="navbar-end"> <div className="navbar-end">
<a className="navbar-item is-hidden-desktop-only"></a> <a className="navbar-item is-hidden-desktop-only"></a>
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i>
{downloadProgressTick && <div className="pulsating-circle"></div>}
</a>
{!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} />
</a>
</div>
) : null}
</div>
{!isUndefined(libraryQueueImportStatus) &&
location.hash !== "#/import" ? (
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-file-import has-text-warning-dark"></i>
</a>
<div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item">
<ul>
{successfulImportJobCount > 0 ? (
<li className="mb-2">
<span className="tag is-success mr-2">
{successfulImportJobCount}
</span>
imported.
</li>
) : null}
{failedImportJobCount > 0 ? (
<li>
<span className="tag is-danger mr-2">
{failedImportJobCount}
</span>
failed to import.
</li>
) : null}
</ul>
</a>
</div>
</div>
) : null}
{/* AirDC++ socket connection status */} {/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable">
{airDCPPSocketConnectionStatus ? (
<>
<a className="navbar-link is-arrowless has-text-success">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right airdcpp-status is-boxed">
{/* AirDC++ Session Information */}
<p>
Last login was{" "}
<span className="tag">
{format(
fromUnixTime(airDCPPSessionInfo.user.last_login),
"dd MMMM, yyyy",
)}
</span>
</p>
<hr className="navbar-divider" />
<p>
<span className="tag has-text-success">
{airDCPPSessionInfo.user.username}
</span>
connected to{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.system_info.client_version}
</span>{" "}
with session ID{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.session_id}
</span>
</p>
{/* <pre>{JSON.stringify(airDCPPSessionInfo, null, 2)}</pre> */}
</div>
</>
) : (
<>
<a className="navbar-link is-arrowless has-text-danger">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right is-boxed">
<pre>
{JSON.stringify(socketDisconnectionReason, null, 2)}
</pre>
</div>
</>
)}
</div>
<div className="navbar-item has-dropdown is-hoverable is-mega"> <div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div> <div className="navbar-link flex">Blog</div>

View File

@@ -143,6 +143,8 @@ export const SETTINGS_CALL_FAILED = "SETTINGS_CALL_FAILED";
export const SETTINGS_OBJECT_DELETED = "SETTINGS_OBJECT_DELETED"; export const SETTINGS_OBJECT_DELETED = "SETTINGS_OBJECT_DELETED";
export const SETTINGS_DB_FLUSH_SUCCESS = "SETTINGS_DB_FLUSH_SUCCESS"; export const SETTINGS_DB_FLUSH_SUCCESS = "SETTINGS_DB_FLUSH_SUCCESS";
export const SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED = "SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED";
// Metron Metadata // Metron Metadata
export const METRON_DATA_FETCH_SUCCESS = "METRON_DATA_FETCH_SUCCESS"; export const METRON_DATA_FETCH_SUCCESS = "METRON_DATA_FETCH_SUCCESS";
export const METRON_DATA_FETCH_IN_PROGRESS = "METRON_DATA_FETCH_IN_PROGRESS"; export const METRON_DATA_FETCH_IN_PROGRESS = "METRON_DATA_FETCH_IN_PROGRESS";

View File

@@ -84,3 +84,10 @@ export const JOB_QUEUE_SERVICE_BASE_URI = hostURIBuilder({
port: "3000", port: "3000",
apiPath: `/api/jobqueue`, apiPath: `/api/jobqueue`,
}); });
export const QBITTORRENT_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3060",
apiPath: `/api/qbittorrent`,
});

View File

@@ -37,6 +37,34 @@
"displayName": "Additional Configuration" "displayName": "Additional Configuration"
} }
] ]
},
{
"id": "qbittorrent",
"displayName": "qBittorrent",
"children": [
{
"id": "qbt-connection",
"displayName": "Connection"
},
{
"id": "qbt-additional-configuration",
"displayName": "qbt-configuration"
}
]
},
{
"id": "prowlarr",
"displayName": "Prowlarr",
"children": [
{
"id": "prowlarr-connection",
"displayName": "Connection"
},
{
"id": "prowlarr-additional-configuration",
"displayName": "prowlarr-configuration"
}
]
} }
] ]
}, },

View File

@@ -1,12 +1,12 @@
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import React, { createContext, useEffect, useState } from "react"; import React, { createContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useQuery, useMutation } from "@tanstack/react-query";
import { toggleAirDCPPSocketConnectionStatus } from "../actions/airdcpp.actions"; import { useStore } from "../store/index";
import { getSettings } from "../actions/settings.actions";
import AirDCPPSocket from "../services/DcppSearchService"; import AirDCPPSocket from "../services/DcppSearchService";
import axios from "axios";
const AirDCPPSocketContextProvider = ({ children }) => { const AirDCPPSocketContextProvider = ({ children }) => {
const { getState, setState } = useStore;
// setter for settings for use in the context consumer // setter for settings for use in the context consumer
const setSettings = (settingsObject) => { const setSettings = (settingsObject) => {
persistSettings({ persistSettings({
@@ -14,11 +14,11 @@ const AirDCPPSocketContextProvider = ({ children }) => {
airDCPPState: { airDCPPState: {
settings: settingsObject, settings: settingsObject,
socket: {}, socket: {},
socketConectionInformation: {}, socketConnectionInformation: {},
}, },
}); });
}; };
// 1. default zero-state for AirDC++ configuration // Initial state for AirDC++ configuration
const initState = { const initState = {
airDCPPState: { airDCPPState: {
settings: {}, settings: {},
@@ -27,59 +27,59 @@ const AirDCPPSocketContextProvider = ({ children }) => {
}, },
setSettings: setSettings, setSettings: setSettings,
}; };
const dispatch = useDispatch();
const [airDCPPState, persistSettings] = useState(initState); const [airDCPPState, persistSettings] = useState(initState);
const airDCPPSettings = useSelector(
(state: RootState) => state.settings.data,
);
// 1. get settings from mongo // 1. get settings from mongo
useEffect(() => { const { data, isLoading, isError } = useQuery({
dispatch(getSettings()); queryKey: ["settings"],
}, []); queryFn: async () =>
await axios({
url: "http://localhost:3000/api/settings/getAllSettings",
method: "GET",
}),
});
const directConnectConfiguration = data?.data.directConnect.client.host;
// 2. If available, init AirDC++ Socket with those settings // 2. If available, init AirDC++ Socket with those settings
useEffect(() => { useEffect(() => {
if (!isEmpty(airDCPPSettings)) { if (!isEmpty(directConnectConfiguration)) {
initializeAirDCPPSocket(airDCPPSettings); initializeAirDCPPSocket(directConnectConfiguration);
} }
}, [airDCPPSettings]); }, [directConnectConfiguration]);
// Method to init AirDC++ Socket with supplied settings // Method to init AirDC++ Socket with supplied settings
const initializeAirDCPPSocket = async (configuration) => { const initializeAirDCPPSocket = async (configuration) => {
console.log("[AirDCPP]: Initializing socket..."); console.log("[AirDCPP]: Initializing socket...");
const {
directConnect: {
client: { host },
},
} = configuration;
const initializedAirDCPPSocket = new AirDCPPSocket({ const initializedAirDCPPSocket = new AirDCPPSocket({
protocol: `${host.protocol}`, protocol: `${configuration.protocol}`,
hostname: `${host.hostname}:${host.port}`, hostname: `${configuration.hostname}:${configuration.port}`,
username: `${host.username}`, username: `${configuration.username}`,
password: `${host.password}`, password: `${configuration.password}`,
}); });
// connect and disconnect handlers // Set up connect and disconnect handlers
initializedAirDCPPSocket.onConnected = (sessionInfo) => { initializedAirDCPPSocket.onConnected = (sessionInfo) => {
dispatch(toggleAirDCPPSocketConnectionStatus("connected", sessionInfo)); // update global state with socket connection status
setState({
airDCPPSocketConnected: true,
});
}; };
initializedAirDCPPSocket.onDisconnected = async ( initializedAirDCPPSocket.onDisconnected = async (
reason, reason,
code, code,
wasClean, wasClean,
) => { ) => {
dispatch( // update global state with socket connection status
toggleAirDCPPSocketConnectionStatus("disconnected", { setState({
reason, disconnectionInfo: { reason, code, wasClean },
code, airDCPPSocketConnected: false,
wasClean, });
}),
);
}; };
// Attempt connection
const socketConnectionInformation = await initializedAirDCPPSocket.connect(); const socketConnectionInformation =
await initializedAirDCPPSocket.connect();
// update the state with the new socket connection information // update the state with the new socket connection information
persistSettings({ persistSettings({
@@ -92,6 +92,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
}); });
}; };
console.log("connected?", getState());
// the Provider gives access to the context to its children // the Provider gives access to the context to its children
return ( return (
<AirDCPPSocketContext.Provider value={airDCPPState}> <AirDCPPSocketContext.Provider value={airDCPPState}>
@@ -101,7 +102,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
}; };
const AirDCPPSocketContext = createContext({ const AirDCPPSocketContext = createContext({
airDCPPState: {}, airDCPPState: {},
saveSettings: () => { }, saveSettings: () => {},
}); });
export { AirDCPPSocketContext, AirDCPPSocketContextProvider }; export { AirDCPPSocketContext, AirDCPPSocketContextProvider };

View File

@@ -1,18 +1,30 @@
import React from "react"; import React from "react";
import { render } from "react-dom"; import { render } from "react-dom";
import { Provider, connect } from "react-redux";
import { HistoryRouter as Router } from "redux-first-history/rr6";
import { store, history } from "./store/index";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./components/App"; import App from "./components/App";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Settings from "./components/Settings/Settings";
const rootEl = document.getElementById("root"); const rootEl = document.getElementById("root");
const root = createRoot(rootEl); const root = createRoot(rootEl);
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "/settings",
element: <Settings />,
},
]);
root.render( root.render(
<Provider store={store}> <QueryClientProvider client={queryClient}>
<Router history={history}> <RouterProvider router={router} />
<App /> <ReactQueryDevtools initialIsOpen={true} />
</Router> </QueryClientProvider>,
</Provider>,
); );

View File

@@ -1,11 +1,11 @@
import comicinfoReducer from "../reducers/comicinfo.reducer"; import comicinfoReducer from "../reducers/comicinfo.reducer";
import fileOpsReducer from "../reducers/fileops.reducer"; import fileOpsReducer from "../reducers/fileops.reducer";
import airdcppReducer from "../reducers/airdcpp.reducer"; import airdcppReducer from "../reducers/airdcpp.reducer";
import settingsReducer from "../reducers/settings.reducer"; // import settingsReducer from "../reducers/settings.reducer";
export const reducers = { export const reducers = {
comicInfo: comicinfoReducer, comicInfo: comicinfoReducer,
fileOps: fileOpsReducer, fileOps: fileOpsReducer,
airdcpp: airdcppReducer, airdcpp: airdcppReducer,
settings: settingsReducer, // settings: settingsReducer,
}; };

View File

@@ -1,49 +1,67 @@
import { import { createSlice, PayloadAction } from "@reduxjs/toolkit";
SETTINGS_CALL_FAILED, import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
SETTINGS_OBJECT_FETCHED, import { RootState } from "../store";
SETTINGS_OBJECT_DELETED, import { isUndefined } from "lodash";
SETTINGS_CALL_IN_PROGRESS, import { SETTINGS_SERVICE_BASE_URI } from "../constants/endpoints";
SETTINGS_DB_FLUSH_SUCCESS,
} from "../constants/action-types"; export interface InitialState {
const initialState = { data: object;
inProgress: boolean;
dbFlushed: boolean;
torrentsList: Array<any>;
}
const initialState: InitialState = {
data: {}, data: {},
inProgress: false, inProgress: false,
DbFlushed: false, dbFlushed: false,
torrentsList: [],
}; };
function settingsReducer(state = initialState, action) { export const settingsSlice = createSlice({
switch (action.type) { name: "settings",
case SETTINGS_CALL_IN_PROGRESS: initialState,
return { reducers: {
...state, SETTINGS_CALL_IN_PROGRESS: (state) => {
inProgress: true, state.inProgress = true;
}; },
case SETTINGS_OBJECT_FETCHED: SETTINGS_OBJECT_FETCHED: (state, action) => {
return { state.data = action.payload;
...state, state.inProgress = false;
data: action.data, },
inProgress: false,
};
case SETTINGS_OBJECT_DELETED: SETTINGS_OBJECT_DELETED: (state, action) => {
return { state.data = action.payload;
...state, state.inProgress = false;
data: action.data, },
inProgress: false,
};
case SETTINGS_DB_FLUSH_SUCCESS: SETTINGS_DB_FLUSH_SUCCESS: (state, action) => {
state.dbFlushed = action.payload;
state.inProgress = false;
},
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED: (state, action) => {
console.log(state); console.log(state);
return { console.log(action);
...state, state.torrentsList = action.payload;
DbFlushed: action.data, },
inProgress: false, },
}; });
default: export const {
return { ...state }; SETTINGS_CALL_IN_PROGRESS,
SETTINGS_OBJECT_FETCHED,
SETTINGS_OBJECT_DELETED,
SETTINGS_DB_FLUSH_SUCCESS,
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
} = settingsSlice.actions;
// Other code such as selectors can use the imported `RootState` type
export const torrentsList = (state: RootState) => state.settings.torrentsList;
export const qBittorrentSettings = (state: RootState) => {
console.log(state);
if (!isUndefined(state.settings?.data?.bittorrent)) {
return state.settings?.data?.bittorrent.client.host;
} }
} };
export default settingsSlice.reducer;
export default settingsReducer;

View File

@@ -0,0 +1,14 @@
import { isNil, isUndefined } from "lodash";
export const hostNameValidator = (hostname: string): string | undefined => {
// https://stackoverflow.com/a/3824105/656708
const hostnameRegex =
/^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/;
if (!isUndefined(hostname)) {
const matches = hostname.match(hostnameRegex);
return !isNil(matches) && matches.length > 0
? undefined
: "Enter a valid hostname";
}
};

View File

@@ -1,33 +1,106 @@
import { createStore, combineReducers, applyMiddleware } from "redux"; import { create } from "zustand";
import { createHashHistory } from "history"; import { isEmpty } from "lodash";
import { composeWithDevTools } from "@redux-devtools/extension"; import AirDCPPSocket from "../services/DcppSearchService";
import thunk from "redux-thunk"; import axios from "axios";
import { createReduxHistoryContext } from "redux-first-history";
import { reducers } from "../reducers/index";
import socketIoMiddleware from "redux-socket.io-middleware";
import socketIOMiddleware from "../shared/middleware/SocketIOMiddleware";
import socketIOConnectionInstance from "../shared/socket.io/instance";
const customSocketIOMiddleware = socketIOMiddleware(socketIOConnectionInstance); export const useStore = create((set, get) => ({
// AirDC++ state
airDCPPSocketConnected: false,
airDCPPDisconnectionInfo: {},
airDCPPClientConfiguration: {},
airDCPPSocketConnectionInformation: {},
setAirDCPPSocketConnectionStatus: () =>
set((value) => ({
airDCPPSocketConnected: value,
})),
getAirDCPPConnectionStatus: () => {
const airDCPPSocketConnectionStatus = get().airDCPPSocketConnected;
},
// Socket.io state
}));
const { createReduxHistory, routerMiddleware, routerReducer } = const { getState, setState } = useStore;
createReduxHistoryContext({
history: createHashHistory(), // Method to init AirDC++ Socket with supplied settings
const initializeAirDCPPSocket = async (configuration) => {
console.log("[AirDCPP]: Initializing socket...");
const initializedAirDCPPSocket = new AirDCPPSocket({
protocol: `${configuration.protocol}`,
hostname: `${configuration.hostname}:${configuration.port}`,
username: `${configuration.username}`,
password: `${configuration.password}`,
}); });
export const store = createStore( // Set up connect and disconnect handlers
combineReducers({ initializedAirDCPPSocket.onConnected = (sessionInfo) => {
router: routerReducer, // update global state with socket connection status
...reducers, setState({
}), airDCPPSocketConnected: true,
composeWithDevTools( });
applyMiddleware( };
socketIoMiddleware(socketIOConnectionInstance), initializedAirDCPPSocket.onDisconnected = async (reason, code, wasClean) => {
customSocketIOMiddleware, // update global state with socket connection status
thunk, setState({
routerMiddleware, disconnectionInfo: { reason, code, wasClean },
), airDCPPSocketConnected: false,
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), });
), };
); // AirDC++ Socket-related connection and post-connection
export const history = createReduxHistory(store); // Attempt connection
const airDCPPSocketConnectionInformation =
await initializedAirDCPPSocket.connect();
setState({
airDCPPSocketConnectionInformation,
});
// Set up event listeners
initializedAirDCPPSocket.addListener(
`queue`,
"queue_bundle_tick",
async (downloadProgressData) => {
console.log(downloadProgressData);
},
);
initializedAirDCPPSocket.addListener(
"queue",
"queue_bundle_added",
async (data) => {
console.log("JEMEN:", data);
},
);
initializedAirDCPPSocket.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.`);
count += 1;
}
}
},
);
};
// 1. get settings from mongo
const { data } = await axios({
url: "http://localhost:3000/api/settings/getAllSettings",
method: "GET",
});
const directConnectConfiguration = data?.directConnect.client.host;
// 2. If available, init AirDC++ Socket with those settings
if (!isEmpty(directConnectConfiguration)) {
initializeAirDCPPSocket(directConnectConfiguration);
setState({
airDCPPClientConfiguration: directConnectConfiguration,
});
}
console.log("connected?", getState());

View File

@@ -4,7 +4,7 @@ import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react'; import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Card } from '../components/Carda'; import { Card } from '../components/shared/Carda';
import "../assets/scss/App.scss"; import "../assets/scss/App.scss";
export default { export default {
/* 👇 The title prop is optional. /* 👇 The title prop is optional.

View File

@@ -6,6 +6,11 @@ export default defineConfig({
publicDir: "public", publicDir: "public",
base: "", base: "",
build: "esnext", build: "esnext",
esbuild: {
supported: {
"top-level-await": true, //browsers can handle top-level-await features
},
},
server: { host: true }, server: { host: true },
plugins: [ plugins: [
nodeResolve({ nodeResolve({

6821
yarn.lock

File diff suppressed because it is too large Load Diff