Compare commits
33 Commits
main
...
qbittorren
| Author | SHA1 | Date | |
|---|---|---|---|
| 90690c6876 | |||
| 08ff21a987 | |||
| 07724380f7 | |||
| 5b27658dc3 | |||
| 91d0110de2 | |||
| 730141ff26 | |||
| 59801763e6 | |||
| 7639833757 | |||
| a0f7280fbb | |||
| 1f8dded15e | |||
| c3b9ad9d91 | |||
| e89a7f9c91 | |||
| 2a6eeaf943 | |||
| d45c53dff9 | |||
| 2df0fce792 | |||
| 4ea9086e3f | |||
| 206c2eeb4b | |||
| 6cd6612ce1 | |||
| 538ca61fa8 | |||
| 69cebe82dd | |||
| c6e55a90ce | |||
| 3ca15148f5 | |||
| bd35223f0f | |||
| 4e82bc73e1 | |||
| ac80c23fa8 | |||
| f9f3109132 | |||
| 111962acd1 | |||
| 502ed707ab | |||
| 0058df3859 | |||
| 8c4027f383 | |||
| 9fe69d21db | |||
| c591f1db2f | |||
| 093dcc448c |
@@ -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",
|
||||||
|
|||||||
51
jsdoc.json
51
jsdoc.json
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
57
package.json
57
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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";
|
||||||
|
|
||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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();
|
||||||
@@ -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">
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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 (
|
||||||
@@ -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 => {
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>,
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
14
src/client/shared/utils/validator.utils.ts
Normal file
14
src/client/shared/utils/validator.utils.ts
Normal 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";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user