Compare commits
63 Commits
dcpp-socke
...
dark-mode-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7739d1ced7 | |||
| 0d438ab913 | |||
| a7b9138f52 | |||
| 0872aa4d8a | |||
| 4d1d29a361 | |||
| b1d8951842 | |||
| 1180a2b42c | |||
| 5ffcb0d642 | |||
| f293d9e3ba | |||
| 1f6d6ca6b6 | |||
| a7508df0b3 | |||
| 81b590157e | |||
| 72a308801d | |||
| e1be413ff6 | |||
| 435056048b | |||
|
|
68442894d0 | ||
| 026d5832c5 | |||
| a068b82db3 | |||
| c74c443534 | |||
| 701d216e44 | |||
| 4034c4e2b4 | |||
| 9c4f2ae9b0 | |||
| a07c0357f0 | |||
| 4215470871 | |||
| 38599fbc78 | |||
| 7f3a1caa3c | |||
| d36138c800 | |||
| 1ed6a622d4 | |||
| 29e0772a10 | |||
| 57b713aca1 | |||
| dfd99e45b6 | |||
| 591ecb394c | |||
| 145427d3fd | |||
| dba520b4c1 | |||
| ef75dad4e2 | |||
| 8bebffd95e | |||
|
|
1bd3d611e4 | ||
|
|
825782fe13 | ||
| 32f4055daa | |||
|
|
c20f24b1a2 | ||
|
|
e4fc28f698 | ||
|
|
d6c183339f | ||
|
|
0eba47e20f | ||
| 928bfd573e | |||
| b8fb179ac6 | |||
| 94692bb6d4 | |||
| 4b795aca5d | |||
| fbf6bed4fe | |||
|
|
18b18c3d81 | ||
| a939bf4c71 | |||
| 0a48ecbb2c | |||
| c5dd1abcdd | |||
| 79f9b22fad | |||
|
|
e291b4806b | ||
| ff7b7c9207 | |||
| 2d61fa1436 | |||
| b12849efda | |||
| fb2c969c00 | |||
| d437fddd1a | |||
| 8dd68e9d54 | |||
|
|
a8b52d0ac6 | ||
| 719ebe7c6e | |||
| df1fbc7dd3 |
17
.babelrc
17
.babelrc
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
"@babel/preset-react",
|
|
||||||
"@babel/preset-typescript"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/transform-runtime",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"@babel/plugin-syntax-top-level-await"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"production": {
|
|
||||||
"presets": ["minify"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:css-modules/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended"],
|
extends: ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:css-modules/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended"],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true // Allows for the parsing of JSX
|
jsx: true // Allows for the parsing of JSX
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: ["@typescript-eslint", "css-modules"],
|
plugins: ["@typescript-eslint", "css-modules"],
|
||||||
settings: {
|
settings: {
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
@@ -18,9 +18,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
react: {
|
react: {
|
||||||
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
|
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fine tune rules
|
// Fine tune rules
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-var-requires": 0
|
"@typescript-eslint/no-var-requires": 0
|
||||||
|
|||||||
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- name: Publish to Registry
|
- name: Publish to Registry
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
uses: elgohr/Publish-Docker-Github-Action@v5
|
||||||
with:
|
with:
|
||||||
name: frishi/threetwo
|
name: frishi/threetwo
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
stories: [
|
|
||||||
"../src/client/stories/*.stories.mdx",
|
|
||||||
"../src/client/stories/*.stories.@(js|jsx|ts|tsx)",
|
|
||||||
],
|
|
||||||
staticDirs: [
|
|
||||||
"../src/client/stories/assets"
|
|
||||||
],
|
|
||||||
addons: [
|
|
||||||
"@storybook/addon-links",
|
|
||||||
"@storybook/preset-scss",
|
|
||||||
"@storybook/addon-essentials",
|
|
||||||
"@storybook/addon-interactions",
|
|
||||||
],
|
|
||||||
framework: "@storybook/react",
|
|
||||||
core: {
|
|
||||||
builder: "webpack5",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
19
.storybook/main.ts
Normal file
19
.storybook/main.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { StorybookConfig } from "@storybook/react-vite";
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||||
|
addons: [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@storybook/addon-onboarding",
|
||||||
|
"@storybook/addon-interactions",
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: "@storybook/react-vite",
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: "tag",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
3
.storybook/preview-head.html
Normal file
3
.storybook/preview-head.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<script>
|
||||||
|
window.global = window;
|
||||||
|
</script>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export const parameters = {
|
|
||||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
18
.storybook/preview.ts
Normal file
18
.storybook/preview.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Preview } from "@storybook/react";
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
default: "light",
|
||||||
|
},
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1013 KiB |
BIN
Dashboard.png
BIN
Dashboard.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 MiB |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:17.3-alpine
|
FROM node:18.15.0-alpine
|
||||||
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
|
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
|
||||||
|
|
||||||
WORKDIR /threetwo
|
WORKDIR /threetwo
|
||||||
@@ -9,11 +9,11 @@ COPY nodemon.json ./
|
|||||||
COPY jsdoc.json ./
|
COPY jsdoc.json ./
|
||||||
|
|
||||||
# RUN apt-get update && apt-get install -y git python3 build-essential autoconf automake g++ libpng-dev make
|
# RUN apt-get update && apt-get install -y git python3 build-essential autoconf automake g++ libpng-dev make
|
||||||
RUN apk --no-cache add g++ make libpng-dev python3 git libc6-compat autoconf automake bash libjpeg-turbo-dev libpng-dev mesa-dev mesa libxi build-base gcc libtool nasm
|
RUN apk --no-cache add g++ make libpng-dev git python3 libc6-compat autoconf automake libjpeg-turbo-dev libpng-dev mesa-dev mesa libxi build-base gcc libtool nasm
|
||||||
RUN yarn --ignore-engines
|
RUN yarn --ignore-engines
|
||||||
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
EXPOSE 3050
|
EXPOSE 5173
|
||||||
|
|
||||||
ENTRYPOINT [ "npm", "start" ]
|
ENTRYPOINT [ "npm", "start" ]
|
||||||
BIN
Library.png
BIN
Library.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
13
README.md
13
README.md
@@ -6,13 +6,13 @@ ThreeTwo! _aims to be_ a comic book curation app.
|
|||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### 🦄 Early Development Support Channel
|
### 🦄 Early Development Support Channel
|
||||||
@@ -40,9 +40,8 @@ For debugging and troubleshooting, you can run this app locally using these step
|
|||||||
|
|
||||||
1. Clone this repo using `git clone https://github.com/rishighan/threetwo.git`
|
1. Clone this repo using `git clone https://github.com/rishighan/threetwo.git`
|
||||||
2. `yarn run dev` (you can ignore the warnings)
|
2. `yarn run dev` (you can ignore the warnings)
|
||||||
3. This will open `http://localhost:3050` in your default browser
|
3. This will open `http://localhost:5173` in your default browser
|
||||||
4. For testing `OPDS` functionality, create a folder called `comics` under `/src/server` and put some comics in there. The `OPDS` feed is accessed to `http://localhost:8050/api/opds`
|
4. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
|
||||||
5. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
|
|
||||||
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
1
funding.yml
Normal file
1
funding.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: [rishighan]
|
||||||
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<title>Three Two!</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="dark:bg-slate-600">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/client/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
193
package.json
193
package.json
@@ -5,181 +5,122 @@
|
|||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"typings": "server/index.js",
|
"typings": "server/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "vite build",
|
||||||
"start": "npm run build && npm run server",
|
"dev": "rimraf dist && npm run build && vite",
|
||||||
"client": "webpack serve --mode development --devtool inline-source-map --hot",
|
"start": "npm run build && vite",
|
||||||
"server": "tsc -p tsconfig.server.json && node server/",
|
|
||||||
"dev": "concurrently \"nodemon\" \"npm run client\"",
|
|
||||||
"server-dev": "nodemon",
|
|
||||||
"docs": "jsdoc -c jsdoc.json",
|
"docs": "jsdoc -c jsdoc.json",
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "build-storybook"
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"author": "Rishi Ghan",
|
"author": "Rishi Ghan",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.13.17",
|
"@dnd-kit/core": "^6.0.8",
|
||||||
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
|
"@dnd-kit/sortable": "^7.0.2",
|
||||||
"@dnd-kit/core": "^4.0.0",
|
"@dnd-kit/utilities": "^3.2.1",
|
||||||
"@dnd-kit/sortable": "^5.0.0",
|
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||||
"@dnd-kit/utilities": "^3.2.0",
|
"@redux-devtools/extension": "^3.2.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
"@parcel/config-default": "^2.6.0",
|
"@tanstack/react-query": "^5.0.5",
|
||||||
"@redux-devtools/extension": "^3.2.2",
|
"@tanstack/react-table": "^8.9.3",
|
||||||
"@tanstack/react-table": "^8.5.11",
|
|
||||||
"@types/mime-types": "^2.1.0",
|
"@types/mime-types": "^2.1.0",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@types/react-redux": "^7.1.16",
|
"airdcpp-apisocket": "^2.5.0-beta.2",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"axios": "^1.3.4",
|
||||||
"@types/sharp": "^0.28.0",
|
"axios-cache-interceptor": "^1.0.1",
|
||||||
"@types/socket.io": "^3.0.2",
|
|
||||||
"@types/socket.io-client": "^3.0.0",
|
|
||||||
"airdcpp-apisocket": "^2.4.4",
|
|
||||||
"array-sort-by": "^1.2.1",
|
|
||||||
"axios": "^0.27.2",
|
|
||||||
"axios-rate-limit": "^1.3.0",
|
"axios-rate-limit": "^1.3.0",
|
||||||
"axios-simple-cache-adapter": "^1.1.0",
|
"babel-plugin-styled-components": "^2.1.4",
|
||||||
"babel-polyfill": "^6.26.0",
|
|
||||||
"babel-preset-minify": "^0.5.2",
|
|
||||||
"better-docs": "^2.7.2",
|
|
||||||
"comlink-loader": "^2.0.0",
|
|
||||||
"compromise": "^13.11.3",
|
|
||||||
"compromise-dates": "^2.2.1",
|
|
||||||
"compromise-numbers": "^1.4.0",
|
|
||||||
"compromise-sentences": "^0.3.0",
|
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"ellipsize": "^0.1.0",
|
"ellipsize": "^0.5.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fastest-validator": "^1.11.0",
|
|
||||||
"filename-parser": "^1.0.2",
|
"filename-parser": "^1.0.2",
|
||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"final-form-arrays": "^3.0.2",
|
"final-form-arrays": "^3.0.2",
|
||||||
|
"history": "^5.3.0",
|
||||||
"html-to-text": "^8.1.0",
|
"html-to-text": "^8.1.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
"jsdoc": "^3.6.10",
|
"jsdoc": "^3.6.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"opds-extra": "^3.0.9",
|
|
||||||
"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",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-collapsible": "^2.9.0",
|
"react-collapsible": "^2.9.0",
|
||||||
"react-comic-viewer": "^0.4.0",
|
"react-comic-viewer": "^0.4.0",
|
||||||
"react-day-picker": "^8.0.6",
|
"react-day-picker": "^8.6.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-final-form": "^6.5.9",
|
"react-final-form": "^6.5.9",
|
||||||
"react-final-form-arrays": "^3.1.3",
|
"react-final-form-arrays": "^3.1.4",
|
||||||
"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": "^7.2.6",
|
"react-router": "^6.9.0",
|
||||||
"react-router": "^6.2.2",
|
"react-router-dom": "^6.9.0",
|
||||||
"react-router-dom": "^6.2.2",
|
"react-select": "^5.8.0",
|
||||||
"react-select": "^5.3.2",
|
"react-select-async-paginate": "^0.7.2",
|
||||||
"react-select-async-paginate": "^0.6.1",
|
|
||||||
"react-slick": "^0.29.0",
|
|
||||||
"react-sliding-pane": "^7.1.0",
|
"react-sliding-pane": "^7.1.0",
|
||||||
"react-stickynode": "^4.0.0",
|
"react-stickynode": "^4.1.0",
|
||||||
"react-textarea-autosize": "^8.3.4",
|
"react-textarea-autosize": "^8.3.4",
|
||||||
"reapop": "^4.0.5",
|
"reapop": "^4.2.1",
|
||||||
"redux-first-history": "^5.0.9",
|
|
||||||
"redux-socket.io-middleware": "^1.0.4",
|
|
||||||
"redux-thunk": "^2.4.1",
|
|
||||||
"sharp": "^0.30.5",
|
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"socket.io-client": "^4.3.2",
|
"socket.io-client": "^4.3.2",
|
||||||
"styled-components": "^5.3.5",
|
"styled-components": "^6.1.0",
|
||||||
"threetwo-ui-typings": "^1.0.14",
|
"threetwo-ui-typings": "^1.0.14",
|
||||||
"voca": "^1.4.0",
|
"vite": "^5.0.5",
|
||||||
|
"vite-plugin-html": "^3.2.0",
|
||||||
"websocket": "^1.0.34",
|
"websocket": "^1.0.34",
|
||||||
"ws": "^7.5.3",
|
"zustand": "^4.4.6"
|
||||||
"xml2js": "^0.4.23",
|
|
||||||
"xregexp": "^5.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.13.10",
|
"@iconify-json/solar": "^1.1.8",
|
||||||
"@babel/core": "^7.13.10",
|
"@iconify/tailwind": "^0.1.4",
|
||||||
"@babel/plugin-syntax-top-level-await": "^7.14.5",
|
"@storybook/addon-essentials": "^7.4.1",
|
||||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
"@storybook/addon-interactions": "^7.4.1",
|
||||||
"@babel/preset-env": "^7.13.10",
|
"@storybook/addon-links": "^7.4.1",
|
||||||
"@babel/preset-react": "^7.12.13",
|
"@storybook/addon-onboarding": "^1.0.8",
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
"@storybook/blocks": "^7.4.1",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
"@storybook/react": "^7.4.1",
|
||||||
"@storybook/addon-actions": "^6.5.9",
|
"@storybook/react-vite": "^7.4.1",
|
||||||
"@storybook/addon-essentials": "^6.5.9",
|
"@storybook/testing-library": "^0.2.0",
|
||||||
"@storybook/addon-interactions": "^6.5.9",
|
"@tanstack/eslint-plugin-query": "^5.0.5",
|
||||||
"@storybook/addon-links": "^6.5.9",
|
"@tanstack/react-query-devtools": "^5.1.0",
|
||||||
"@storybook/builder-webpack4": "^6.5.9",
|
|
||||||
"@storybook/builder-webpack5": "^6.5.9",
|
|
||||||
"@storybook/manager-webpack4": "^6.5.9",
|
|
||||||
"@storybook/manager-webpack5": "^6.5.9",
|
|
||||||
"@storybook/preset-scss": "^1.0.3",
|
|
||||||
"@storybook/react": "^6.5.9",
|
|
||||||
"@storybook/testing-library": "^0.0.13",
|
|
||||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
|
||||||
"@tsconfig/node14": "^1.0.0",
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@types/ellipsize": "^0.1.1",
|
||||||
"@types/express": "^4.17.8",
|
"@types/express": "^4.17.8",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@types/node": "^14.14.34",
|
"@types/node": "^14.14.34",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-redux": "^7.1.16",
|
"@types/react-redux": "^7.1.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
"autoprefixer": "^10.4.16",
|
||||||
"@typescript-eslint/parser": "^4.17.0",
|
|
||||||
"awesome-typescript-loader": "^5.2.1",
|
|
||||||
"babel-eslint": "^10.0.0",
|
|
||||||
"babel-loader": "^8.2.2",
|
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"buffer": "^6.0.3",
|
"docdash": "^2.0.2",
|
||||||
"bulma": "^0.9.3",
|
"eslint": "^8.49.0",
|
||||||
"clean-webpack-plugin": "^1.0.0",
|
|
||||||
"comlink": "^4.3.0",
|
|
||||||
"compromise-strict": "^0.0.2",
|
|
||||||
"concurrently": "^4.0.0",
|
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
|
||||||
"css-loader": "^5.1.2",
|
|
||||||
"eslint": "^7.22.0",
|
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
|
||||||
"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",
|
||||||
|
"eslint-plugin-storybook": "^0.6.13",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"fs-extra": "^9.1.0",
|
|
||||||
"html-webpack-plugin": "^5.3.1",
|
|
||||||
"http-response-stream": "^1.0.9",
|
|
||||||
"image-webpack-loader": "^8.1.0",
|
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^29.6.3",
|
||||||
"mini-css-extract-plugin": "^1.4.1",
|
"nodemon": "^3.0.1",
|
||||||
"node-sass": "^7.0.1",
|
"postcss": "^8.4.32",
|
||||||
"nodemon": "^1.17.3",
|
"postcss-import": "^15.1.0",
|
||||||
"npm": "^8.11.0",
|
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^4.1.3",
|
||||||
"sass-loader": "^11.0.1",
|
"sass": "^1.69.5",
|
||||||
"source-map-loader": "^0.2.4",
|
"storybook": "^7.3.2",
|
||||||
"string-similarity": "^4.0.4",
|
"tailwindcss": "^3.3.5",
|
||||||
"style-loader": "^2.0.0",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
"tslint": "^6.1.3",
|
"typescript": "^5.1.6"
|
||||||
"typescript": "^4.2.3",
|
|
||||||
"url-loader": "^1.0.1",
|
|
||||||
"webpack": "^5.73.0",
|
|
||||||
"webpack-bundle-analyzer": "^4.4.2",
|
|
||||||
"webpack-cli": "^4.6.0",
|
|
||||||
"webpack-dev-server": "^4.7.4",
|
|
||||||
"webpack-merge": "^5.7.3"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"@storybook/react/webpack": "^5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
postcss.config.js
Normal file
7
postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-import": {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/fonts/Hasklig-Regular.otf
Normal file
BIN
public/fonts/Hasklig-Regular.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-Heavy.otf
Normal file
BIN
public/fonts/PPObjectSans-Heavy.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-HeavySlanted.otf
Normal file
BIN
public/fonts/PPObjectSans-HeavySlanted.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-Regular.otf
Normal file
BIN
public/fonts/PPObjectSans-Regular.otf
Normal file
Binary file not shown.
BIN
public/fonts/PPObjectSans-Slanted.otf
Normal file
BIN
public/fonts/PPObjectSans-Slanted.otf
Normal file
Binary file not shown.
@@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Three Two!</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -4,7 +4,10 @@ import {
|
|||||||
PriorityEnum,
|
PriorityEnum,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
} from "threetwo-ui-typings";
|
} from "threetwo-ui-typings";
|
||||||
import { LIBRARY_SERVICE_BASE_URI, SEARCH_SERVICE_BASE_URI } from "../constants/endpoints";
|
import {
|
||||||
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
|
SEARCH_SERVICE_BASE_URI,
|
||||||
|
} from "../constants/endpoints";
|
||||||
import {
|
import {
|
||||||
AIRDCPP_SEARCH_RESULTS_ADDED,
|
AIRDCPP_SEARCH_RESULTS_ADDED,
|
||||||
AIRDCPP_SEARCH_RESULTS_UPDATED,
|
AIRDCPP_SEARCH_RESULTS_UPDATED,
|
||||||
@@ -18,6 +21,8 @@ import {
|
|||||||
IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
||||||
AIRDCPP_TRANSFERS_FETCHED,
|
AIRDCPP_TRANSFERS_FETCHED,
|
||||||
LIBRARY_ISSUE_BUNDLES,
|
LIBRARY_ISSUE_BUNDLES,
|
||||||
|
AIRDCPP_SOCKET_CONNECTED,
|
||||||
|
AIRDCPP_SOCKET_DISCONNECTED,
|
||||||
} from "../constants/action-types";
|
} from "../constants/action-types";
|
||||||
import { isNil } from "lodash";
|
import { isNil } from "lodash";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -28,194 +33,126 @@ interface SearchData {
|
|||||||
priority: PriorityEnum;
|
priority: PriorityEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms: number): Promise<NodeJS.Timeout> {
|
export const sleep = (ms: number): Promise<NodeJS.Timeout> => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
};
|
||||||
|
|
||||||
export const search =
|
export const toggleAirDCPPSocketConnectionStatus =
|
||||||
(data: SearchData, ADCPPSocket: any, credentials: any) =>
|
(status: String, payload?: any) => async (dispatch) => {
|
||||||
async (dispatch) => {
|
switch (status) {
|
||||||
try {
|
case "connected":
|
||||||
if (!ADCPPSocket.isConnected()) {
|
|
||||||
await ADCPPSocket.connect(
|
|
||||||
credentials.username,
|
|
||||||
credentials.password,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const instance: SearchInstance = await ADCPPSocket.post("search");
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: AIRDCPP_SEARCH_IN_PROGRESS,
|
type: AIRDCPP_SOCKET_CONNECTED,
|
||||||
|
data: payload,
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
// We want to get notified about every new result in order to make the user experience better
|
case "disconnected":
|
||||||
await ADCPPSocket.addListener(
|
dispatch({
|
||||||
`search`,
|
type: AIRDCPP_SOCKET_DISCONNECTED,
|
||||||
"search_result_added",
|
data: payload,
|
||||||
async (groupedResult) => {
|
});
|
||||||
// ...add the received result in the UI
|
break;
|
||||||
// (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results)
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: AIRDCPP_SEARCH_RESULTS_ADDED,
|
|
||||||
groupedResult,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
instance.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We also want to update the existing items in our list when new hits arrive for the previously listed files/directories
|
|
||||||
await ADCPPSocket.addListener(
|
|
||||||
`search`,
|
|
||||||
"search_result_updated",
|
|
||||||
async (groupedResult) => {
|
|
||||||
// ...update properties of the existing result in the UI
|
|
||||||
dispatch({
|
|
||||||
type: AIRDCPP_SEARCH_RESULTS_UPDATED,
|
|
||||||
groupedResult,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
instance.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We need to show something to the user in case the search won't yield any results so that he won't be waiting forever)
|
|
||||||
// Wait for 5 seconds for any results to arrive after the searches were sent to the hubs
|
|
||||||
await ADCPPSocket.addListener(
|
|
||||||
`search`,
|
|
||||||
"search_hub_searches_sent",
|
|
||||||
async (searchInfo) => {
|
|
||||||
await sleep(5000);
|
|
||||||
|
|
||||||
// Check the number of received results (in real use cases we should know that even without calling the API)
|
|
||||||
const currentInstance = await ADCPPSocket.get(
|
|
||||||
`search/${instance.id}`,
|
|
||||||
);
|
|
||||||
if (currentInstance.result_count === 0) {
|
|
||||||
// ...nothing was received, show an informative message to the user
|
|
||||||
console.log("No more search results.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The search can now be considered to be "complete"
|
|
||||||
// If there's an "in progress" indicator in the UI, that could also be disabled here
|
|
||||||
dispatch({
|
|
||||||
type: AIRDCPP_HUB_SEARCHES_SENT,
|
|
||||||
searchInfo,
|
|
||||||
instance,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
instance.id,
|
|
||||||
);
|
|
||||||
// Finally, perform the actual search
|
|
||||||
await ADCPPSocket.post(`search/${instance.id}/hub_search`, data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log("Can't set AirDC++ socket status.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
export const downloadAirDCPPItem =
|
export const downloadAirDCPPItem =
|
||||||
(searchInstanceId: Number,
|
(
|
||||||
|
searchInstanceId: Number,
|
||||||
resultId: String,
|
resultId: String,
|
||||||
comicObjectId: String,
|
comicObjectId: String,
|
||||||
name: String, size: Number, type: any,
|
name: String,
|
||||||
|
size: Number,
|
||||||
|
type: any,
|
||||||
ADCPPSocket: any,
|
ADCPPSocket: any,
|
||||||
credentials: any,
|
credentials: any,
|
||||||
): void =>
|
): void =>
|
||||||
async (dispatch) => {
|
async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
if (!ADCPPSocket.isConnected()) {
|
if (!ADCPPSocket.isConnected()) {
|
||||||
await ADCPPSocket.connect(
|
await ADCPPSocket.connect();
|
||||||
`${credentials.username}`,
|
|
||||||
`${credentials.password}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let bundleDBImportResult = {};
|
|
||||||
const downloadResult = await ADCPPSocket.post(
|
|
||||||
`search/${searchInstanceId}/results/${resultId}/download`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isNil(downloadResult)) {
|
|
||||||
bundleDBImportResult = await axios({
|
|
||||||
method: "POST",
|
|
||||||
url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
bundleId: downloadResult.bundle_info.id,
|
|
||||||
comicObjectId,
|
|
||||||
name,
|
|
||||||
size,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
|
|
||||||
downloadResult,
|
|
||||||
bundleDBImportResult,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
|
||||||
comicBookDetail: bundleDBImportResult.data,
|
|
||||||
IMS_inProgress: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
let bundleDBImportResult = {};
|
||||||
|
const downloadResult = await ADCPPSocket.post(
|
||||||
|
`search/${searchInstanceId}/results/${resultId}/download`,
|
||||||
|
);
|
||||||
|
|
||||||
export const getBundlesForComic =
|
if (!isNil(downloadResult)) {
|
||||||
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
|
bundleDBImportResult = await axios({
|
||||||
async (dispatch) => {
|
|
||||||
try {
|
|
||||||
if (!ADCPPSocket.isConnected()) {
|
|
||||||
await ADCPPSocket.connect(
|
|
||||||
`${credentials.username}`,
|
|
||||||
`${credentials.password}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const comicObject = await axios({
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
|
url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
id: `${comicObjectId}`,
|
bundleId: downloadResult.bundle_info.id,
|
||||||
|
comicObjectId,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// get only the bundles applicable for the comic
|
|
||||||
if (comicObject.data.acquisition.directconnect) {
|
dispatch({
|
||||||
const filteredBundles = comicObject.data.acquisition.directconnect.downloads.map(
|
type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
|
||||||
|
downloadResult,
|
||||||
|
bundleDBImportResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
||||||
|
comicBookDetail: bundleDBImportResult.data,
|
||||||
|
IMS_inProgress: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBundlesForComic =
|
||||||
|
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
|
||||||
|
async (dispatch) => {
|
||||||
|
try {
|
||||||
|
if (!ADCPPSocket.isConnected()) {
|
||||||
|
await ADCPPSocket.connect();
|
||||||
|
}
|
||||||
|
const comicObject = await axios({
|
||||||
|
method: "POST",
|
||||||
|
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
id: `${comicObjectId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// get only the bundles applicable for the comic
|
||||||
|
if (comicObject.data.acquisition.directconnect) {
|
||||||
|
const filteredBundles =
|
||||||
|
comicObject.data.acquisition.directconnect.downloads.map(
|
||||||
async ({ bundleId }) => {
|
async ({ bundleId }) => {
|
||||||
return await ADCPPSocket.get(`queue/bundles/${bundleId}`);
|
return await ADCPPSocket.get(`queue/bundles/${bundleId}`);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: AIRDCPP_BUNDLES_FETCHED,
|
type: AIRDCPP_BUNDLES_FETCHED,
|
||||||
bundles: await Promise.all(filteredBundles),
|
bundles: await Promise.all(filteredBundles),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getTransfers =
|
export const getTransfers =
|
||||||
(ADCPPSocket: any, credentials: any) => async (dispatch) => {
|
(ADCPPSocket: any, credentials: any) => async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
if (!ADCPPSocket.isConnected()) {
|
if (!ADCPPSocket.isConnected()) {
|
||||||
await ADCPPSocket.connect(
|
await ADCPPSocket.connect();
|
||||||
`${credentials.username}`,
|
|
||||||
`${credentials.password}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const bundles = await ADCPPSocket.get("queue/bundles/1/85", {});
|
const bundles = await ADCPPSocket.get("queue/bundles/1/85", {});
|
||||||
if (!isNil(bundles)) {
|
if (!isNil(bundles)) {
|
||||||
@@ -234,7 +171,6 @@ export const getTransfers =
|
|||||||
type: LIBRARY_ISSUE_BUNDLES,
|
type: LIBRARY_ISSUE_BUNDLES,
|
||||||
issue_bundles,
|
issue_bundles,
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import rateLimiter from "axios-rate-limit";
|
import rateLimiter from "axios-rate-limit";
|
||||||
import {
|
import { setupCache } from "axios-cache-interceptor";
|
||||||
AxiosCacheRequestConfig,
|
|
||||||
createCacheAdapter,
|
|
||||||
} from "axios-simple-cache-adapter";
|
|
||||||
import qs from "qs";
|
|
||||||
import {
|
import {
|
||||||
CV_SEARCH_SUCCESS,
|
CV_SEARCH_SUCCESS,
|
||||||
CV_API_CALL_IN_PROGRESS,
|
CV_API_CALL_IN_PROGRESS,
|
||||||
@@ -26,25 +22,21 @@ import {
|
|||||||
LIBRARY_SERVICE_BASE_URI,
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
} from "../constants/endpoints";
|
} from "../constants/endpoints";
|
||||||
|
|
||||||
const axiosCacheAdapter = createCacheAdapter();
|
|
||||||
const http = rateLimiter(axios.create(), {
|
const http = rateLimiter(axios.create(), {
|
||||||
maxRequests: 1,
|
maxRequests: 1,
|
||||||
perMilliseconds: 1000,
|
perMilliseconds: 1000,
|
||||||
maxRPS: 1,
|
maxRPS: 1,
|
||||||
});
|
});
|
||||||
|
const cachedAxios = setupCache(axios);
|
||||||
export const getWeeklyPullList = (options) => async (dispatch) => {
|
export const getWeeklyPullList = (options) => async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
|
type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
|
||||||
});
|
});
|
||||||
|
await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
|
||||||
await axios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
|
|
||||||
method: "get",
|
method: "get",
|
||||||
params: options,
|
params: options,
|
||||||
axiosCacheAdapter,
|
}).then((response) => {
|
||||||
cache: 1000, // value in MS
|
|
||||||
} as AxiosCacheRequestConfig).then((response) => {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CV_WEEKLY_PULLLIST_FETCHED,
|
type: CV_WEEKLY_PULLLIST_FETCHED,
|
||||||
data: response.data.result,
|
data: response.data.result,
|
||||||
@@ -70,9 +62,6 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
},
|
},
|
||||||
paramsSerializer: (params) => {
|
|
||||||
return qs.stringify(params, { arrayFormat: "repeat" });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (options.callURIAction) {
|
switch (options.callURIAction) {
|
||||||
@@ -137,7 +126,7 @@ export const analyzeLibrary = (issues) => async (dispatch) => {
|
|||||||
queryObjects,
|
queryObjects,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED,
|
type: CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED,
|
||||||
matches: foo.data,
|
matches: foo.data,
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import {
|
|||||||
COMICVINE_SERVICE_URI,
|
COMICVINE_SERVICE_URI,
|
||||||
IMAGETRANSFORMATION_SERVICE_BASE_URI,
|
IMAGETRANSFORMATION_SERVICE_BASE_URI,
|
||||||
LIBRARY_SERVICE_BASE_URI,
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
LIBRARY_SERVICE_HOST,
|
|
||||||
SEARCH_SERVICE_BASE_URI,
|
SEARCH_SERVICE_BASE_URI,
|
||||||
|
JOB_QUEUE_SERVICE_BASE_URI,
|
||||||
} from "../constants/endpoints";
|
} from "../constants/endpoints";
|
||||||
import {
|
import {
|
||||||
IMS_COMIC_BOOK_GROUPS_FETCHED,
|
IMS_COMIC_BOOK_GROUPS_FETCHED,
|
||||||
IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS,
|
IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS,
|
||||||
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
|
|
||||||
IMS_RECENT_COMICS_FETCHED,
|
IMS_RECENT_COMICS_FETCHED,
|
||||||
IMS_WANTED_COMICS_FETCHED,
|
IMS_WANTED_COMICS_FETCHED,
|
||||||
CV_API_CALL_IN_PROGRESS,
|
CV_API_CALL_IN_PROGRESS,
|
||||||
@@ -22,23 +21,38 @@ import {
|
|||||||
LS_IMPORT,
|
LS_IMPORT,
|
||||||
IMG_ANALYSIS_CALL_IN_PROGRESS,
|
IMG_ANALYSIS_CALL_IN_PROGRESS,
|
||||||
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
||||||
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
|
|
||||||
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
||||||
SS_SEARCH_RESULTS_FETCHED,
|
SS_SEARCH_RESULTS_FETCHED,
|
||||||
SS_SEARCH_IN_PROGRESS,
|
SS_SEARCH_IN_PROGRESS,
|
||||||
FILEOPS_STATE_RESET,
|
FILEOPS_STATE_RESET,
|
||||||
LS_IMPORT_CALL_IN_PROGRESS,
|
LS_IMPORT_CALL_IN_PROGRESS,
|
||||||
LS_TOGGLE_IMPORT_QUEUE,
|
|
||||||
SS_SEARCH_FAILED,
|
SS_SEARCH_FAILED,
|
||||||
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||||
WANTED_COMICS_FETCHED,
|
WANTED_COMICS_FETCHED,
|
||||||
VOLUMES_FETCHED,
|
VOLUMES_FETCHED,
|
||||||
CV_WEEKLY_PULLLIST_FETCHED,
|
LIBRARY_SERVICE_HEALTH,
|
||||||
|
LS_SET_QUEUE_STATUS,
|
||||||
|
LS_IMPORT_JOB_STATISTICS_FETCHED,
|
||||||
} from "../constants/action-types";
|
} from "../constants/action-types";
|
||||||
import { success } from "react-notification-system-redux";
|
import { success } from "react-notification-system-redux";
|
||||||
|
|
||||||
import { isNil, map } from "lodash";
|
import { isNil } from "lodash";
|
||||||
|
|
||||||
|
export const getServiceStatus = (serviceName?: string) => async (dispatch) => {
|
||||||
|
axios
|
||||||
|
.request({
|
||||||
|
url: `${LIBRARY_SERVICE_BASE_URI}/getHealthInformation`,
|
||||||
|
method: "GET",
|
||||||
|
transformResponse: (r: string) => JSON.parse(r),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
dispatch({
|
||||||
|
type: LIBRARY_SERVICE_HEALTH,
|
||||||
|
status: data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
export async function walkFolder(path: string): Promise<Array<IFolderData>> {
|
export async function walkFolder(path: string): Promise<Array<IFolderData>> {
|
||||||
return axios
|
return axios
|
||||||
.request<Array<IFolderData>>({
|
.request<Array<IFolderData>>({
|
||||||
@@ -74,19 +88,38 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
|
|||||||
// autoDismiss: 0,
|
// autoDismiss: 0,
|
||||||
// }),
|
// }),
|
||||||
// );
|
// );
|
||||||
|
const sessionId = localStorage.getItem("sessionId");
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LS_IMPORT,
|
type: LS_IMPORT,
|
||||||
meta: { remote: true },
|
});
|
||||||
data: {},
|
|
||||||
|
await axios.request({
|
||||||
|
url: `${LIBRARY_SERVICE_BASE_URI}/newImport`,
|
||||||
|
method: "POST",
|
||||||
|
data: { sessionId },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const toggleImportQueueStatus = (options) => async (dispatch) => {
|
|
||||||
|
export const getImportJobResultStatistics = () => async (dispatch) => {
|
||||||
|
const result = await axios.request({
|
||||||
|
url: `${JOB_QUEUE_SERVICE_BASE_URI}/getJobResultStatistics`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LS_TOGGLE_IMPORT_QUEUE,
|
type: LS_IMPORT_JOB_STATISTICS_FETCHED,
|
||||||
meta: { remote: true },
|
data: result.data,
|
||||||
data: { manjhul: "jigyadam", action: options.action },
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setQueueControl =
|
||||||
|
(queueAction: string, queueStatus: string) => async (dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: LS_SET_QUEUE_STATUS,
|
||||||
|
meta: { remote: true },
|
||||||
|
data: { queueAction, queueStatus },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches comic book metadata for various types
|
* Fetches comic book metadata for various types
|
||||||
* @return metadata for the comic book object categories
|
* @return metadata for the comic book object categories
|
||||||
@@ -127,49 +160,50 @@ export const getComicBooks = (options) => async (dispatch) => {
|
|||||||
* @returns Nothing.
|
* @returns Nothing.
|
||||||
* @param payload
|
* @param payload
|
||||||
*/
|
*/
|
||||||
export const importToDB = (sourceName: string, metadata?: any) => (dispatch) => {
|
export const importToDB =
|
||||||
try {
|
(sourceName: string, metadata?: any) => (dispatch) => {
|
||||||
const comicBookMetadata = {
|
try {
|
||||||
importType: "new",
|
const comicBookMetadata = {
|
||||||
payload: {
|
importType: "new",
|
||||||
rawFileDetails: {
|
payload: {
|
||||||
name: "",
|
rawFileDetails: {
|
||||||
},
|
name: "",
|
||||||
importStatus: {
|
|
||||||
isImported: true,
|
|
||||||
tagged: false,
|
|
||||||
matchedResult: {
|
|
||||||
score: "0",
|
|
||||||
},
|
},
|
||||||
|
importStatus: {
|
||||||
|
isImported: true,
|
||||||
|
tagged: false,
|
||||||
|
matchedResult: {
|
||||||
|
score: "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourcedMetadata: metadata || null,
|
||||||
|
acquisition: { source: { wanted: true, name: sourceName } },
|
||||||
},
|
},
|
||||||
sourcedMetadata: metadata || null,
|
};
|
||||||
acquisition: { source: { wanted: true, name: sourceName } },
|
dispatch({
|
||||||
}
|
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
|
||||||
};
|
|
||||||
dispatch({
|
|
||||||
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
|
|
||||||
});
|
|
||||||
return axios
|
|
||||||
.request({
|
|
||||||
url: `${LIBRARY_SERVICE_BASE_URI}/rawImportToDb`,
|
|
||||||
method: "POST",
|
|
||||||
data: comicBookMetadata,
|
|
||||||
// transformResponse: (r: string) => JSON.parse(r),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
const { data } = response;
|
|
||||||
dispatch({
|
|
||||||
type: IMS_CV_METADATA_IMPORT_SUCCESSFUL,
|
|
||||||
importResult: data,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
return axios
|
||||||
dispatch({
|
.request({
|
||||||
type: IMS_CV_METADATA_IMPORT_FAILED,
|
url: `${LIBRARY_SERVICE_BASE_URI}/rawImportToDb`,
|
||||||
importError: error,
|
method: "POST",
|
||||||
});
|
data: comicBookMetadata,
|
||||||
}
|
// transformResponse: (r: string) => JSON.parse(r),
|
||||||
};
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
dispatch({
|
||||||
|
type: IMS_CV_METADATA_IMPORT_SUCCESSFUL,
|
||||||
|
importResult: data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: IMS_CV_METADATA_IMPORT_FAILED,
|
||||||
|
importError: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchVolumeGroups = () => async (dispatch) => {
|
export const fetchVolumeGroups = () => async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
@@ -254,24 +288,23 @@ export const fetchComicVineMatches =
|
|||||||
* @returns {any}
|
* @returns {any}
|
||||||
*/
|
*/
|
||||||
export const extractComicArchive =
|
export const extractComicArchive =
|
||||||
|
|
||||||
(path: string, options: any): any =>
|
(path: string, options: any): any =>
|
||||||
async (dispatch) => {
|
async (dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
||||||
});
|
});
|
||||||
await axios({
|
await axios({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
|
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
filePath: path,
|
filePath: path,
|
||||||
options,
|
options,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Description
|
* Description
|
||||||
@@ -301,26 +334,26 @@ export const searchIssue = (query, options) => async (dispatch) => {
|
|||||||
case "wantedComicsPage":
|
case "wantedComicsPage":
|
||||||
dispatch({
|
dispatch({
|
||||||
type: WANTED_COMICS_FETCHED,
|
type: WANTED_COMICS_FETCHED,
|
||||||
data: response.data.body,
|
data: response.data.hits,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "globalSearchBar":
|
case "globalSearchBar":
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
type: SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||||
data: response.data.body,
|
data: response.data.hits,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "libraryPage":
|
case "libraryPage":
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SS_SEARCH_RESULTS_FETCHED,
|
type: SS_SEARCH_RESULTS_FETCHED,
|
||||||
data: response.data.body,
|
data: response.data.hits,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "volumesPage":
|
case "volumesPage":
|
||||||
dispatch({
|
dispatch({
|
||||||
type: VOLUMES_FETCHED,
|
type: VOLUMES_FETCHED,
|
||||||
data: response.data.body,
|
data: response.data.hits,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -349,4 +382,4 @@ export const analyzeImage =
|
|||||||
type: IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
type: IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
||||||
result: foo.data,
|
result: foo.data,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,631 +1,15 @@
|
|||||||
@import "../../../../node_modules/bulma/bulma.sass";
|
@tailwind base;
|
||||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
@tailwind components;
|
||||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
@tailwind utilities;
|
||||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
|
|
||||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
|
||||||
$bg-color: yellow;
|
|
||||||
$border-color: red;
|
|
||||||
|
|
||||||
$volume-color: #fdecd1;
|
@layer base {
|
||||||
$issue-color: #f2f1f9;
|
@font-face {
|
||||||
$size-8: 0.9rem;
|
font-family: "PP Object Sans Regular";
|
||||||
$size-9: 0.7rem;
|
src: url("/fonts/PPObjectSans-Regular.otf") format("opentype");
|
||||||
$flexSize: 4em;
|
}
|
||||||
$boxSpacing: 1em;
|
|
||||||
$colorText: #404646;
|
|
||||||
|
|
||||||
.is-size-8 {
|
@font-face {
|
||||||
font-size: $size-8;
|
font-family: "Hasklig Regular";
|
||||||
}
|
src: url("/fonts/Hasklig-Regular.otf") format("opentype");
|
||||||
|
|
||||||
.is-size-9 {
|
|
||||||
font-size: $size-9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-tag {
|
|
||||||
align-items: center;
|
|
||||||
background-color: #fff6de;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #4a4a4a;
|
|
||||||
display: inline-flex;
|
|
||||||
font-size: $size-9;
|
|
||||||
height: 1.5em;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1.5;
|
|
||||||
padding-left: 0.55em;
|
|
||||||
padding-right: 0.55em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// global style overrides
|
|
||||||
|
|
||||||
pre {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
|
||||||
.app {
|
|
||||||
font-family: helvetica, arial, sans-serif;
|
|
||||||
padding: 2em;
|
|
||||||
border: 5px solid $border-color;
|
|
||||||
|
|
||||||
p {
|
|
||||||
background-color: $bg-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Navbar
|
|
||||||
.navbar {
|
|
||||||
border-bottom: 1px solid #f2f1f9;
|
|
||||||
.download-progress-meter {
|
|
||||||
margin-left: -300px;
|
|
||||||
min-width: 500px;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background: #454a59;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #454a59;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulsating-circle {
|
|
||||||
position: relative;
|
|
||||||
left: -120%;
|
|
||||||
top: 20%;
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: 300%;
|
|
||||||
height: 300%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-left: -100%;
|
|
||||||
margin-top: -100%;
|
|
||||||
border-radius: 45px;
|
|
||||||
background-color: #01a4e9;
|
|
||||||
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: green;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
|
||||||
animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-ring {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.33);
|
|
||||||
}
|
|
||||||
80%,
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-dot {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-item.is-mega {
|
|
||||||
position: static;
|
|
||||||
|
|
||||||
.is-mega-menu-title {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding: 0.375rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dashboard
|
|
||||||
|
|
||||||
// slick slider overrides
|
|
||||||
.slick-slider {
|
|
||||||
margin-left: -10px;
|
|
||||||
.slick-list {
|
|
||||||
padding: 0 0px 15px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.recent-comics-container {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -22px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
.recent-comics-column {
|
|
||||||
padding-left: 22px; /* gutter size */
|
|
||||||
background-clip: padding-box;
|
|
||||||
& > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.volumes-container {
|
|
||||||
.stack {
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
/* The top layer shadow */ 0 -1px 1px rgba(0, 0, 0, 0.15),
|
|
||||||
/* The second layer */ 0 -10px 0 -5px #eee,
|
|
||||||
/* The second layer shadow */ 0 -10px 1px -4px rgba(0, 0, 0, 0.15),
|
|
||||||
/* The third layer */ 0 -20px 0 -10px #eee,
|
|
||||||
/* The third layer shadow */ 0 -20px 1px -9px rgba(0, 0, 0, 0.15);
|
|
||||||
img {
|
|
||||||
height: auto;
|
|
||||||
border-top-left-radius: 0.5rem;
|
|
||||||
border-top-right-radius: 0.5rem;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
.stack-title {
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
margin: -5px 0 0 0;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
box-shadow: 1px 8px 23px 7px rgba(0, 0, 0, 0.12);
|
|
||||||
border-bottom-right-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.volumes-grid {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -30px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.volumes-grid-column {
|
|
||||||
padding-left: 22px; /* gutter size */
|
|
||||||
background-clip: padding-box;
|
|
||||||
& > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.min {
|
|
||||||
overflow: visible;
|
|
||||||
margin: auto;
|
|
||||||
.tag__custom {
|
|
||||||
height: auto !important;
|
|
||||||
padding: 0.3rem;
|
|
||||||
white-space: unset !important;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #effaf5;
|
|
||||||
color: #257953;
|
|
||||||
}
|
|
||||||
.tags {
|
|
||||||
display: inline;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-left: 5px;
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
border-radius: 0.4em;
|
|
||||||
margin: 10px 0 10px 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.generic-card {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #fff;
|
|
||||||
border-top-left-radius: 0.4rem;
|
|
||||||
border-top-right-radius: 0.4rem;
|
|
||||||
border-bottom-left-radius: 0.4rem;
|
|
||||||
border-bottom-right-radius: 0.4rem;
|
|
||||||
box-shadow: 1px 8px 23px 7px rgba(0, 0, 0, 0.12);
|
|
||||||
|
|
||||||
.green-border {
|
|
||||||
border: 1px dotted #168b64;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.truncate {
|
|
||||||
width: 100px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.partial-rounded-card-image {
|
|
||||||
figure {
|
|
||||||
display: flex;
|
|
||||||
img {
|
|
||||||
border-top-left-radius: 0.4rem;
|
|
||||||
border-top-right-radius: 0.4rem;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.rounded-card-image {
|
|
||||||
figure {
|
|
||||||
display: flex;
|
|
||||||
img {
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
.card-title {
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
.custom-icon,
|
|
||||||
i {
|
|
||||||
margin: 4px 4px 4px 0;
|
|
||||||
}
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-container {
|
|
||||||
// display: grid;
|
|
||||||
// grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
// column-gap: 0.5em;
|
|
||||||
// row-gap: 1.2em;
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
|
|
||||||
.partial-rounded-card-image {
|
|
||||||
img {
|
|
||||||
border-top-left-radius: 0.4rem;
|
|
||||||
border-top-right-radius: 0.4rem;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.rounded-card-image {
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-horizontal {
|
|
||||||
// margin: $boxSpacing / 2;
|
|
||||||
border-radius: 1.5em;
|
|
||||||
height: $flexSize;
|
|
||||||
max-width: $flexSize * 3;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
.card-image {
|
|
||||||
// leaving this here... for posterity
|
|
||||||
img.image {
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-bottom-left-radius: 8px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
height: 100%;
|
|
||||||
max-width: $flexSize * 1.3;
|
|
||||||
object-fit: cover;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
img.cropped-image {
|
|
||||||
width: 70px;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-bottom-left-radius: 8px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
height: 64px;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: 100% 0;
|
|
||||||
// flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
align-self: top;
|
|
||||||
flex: 1;
|
|
||||||
padding-left: 0.7em;
|
|
||||||
padding-top: 0.4em;
|
|
||||||
padding-bottom: 0em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// raw file details
|
|
||||||
.raw-file-details {
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: beige;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comic-viewer {
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
// comicvine metadata
|
|
||||||
.comicvine-metadata {
|
|
||||||
background-color: #f2f1f9;
|
|
||||||
padding: 0.8rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-metadata {
|
|
||||||
background-color: #fbffee;
|
|
||||||
padding: 0.8em;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
.name {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #4a4f50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comicInfo-metadata {
|
|
||||||
background-color: #f7ebdd;
|
|
||||||
padding: 0.8rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comic Detail
|
|
||||||
.comic-detail {
|
|
||||||
dl {
|
|
||||||
dd {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
.airdcpp-text {
|
|
||||||
margin: 0 0 0 0.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AirDC++ search results
|
|
||||||
.dupe-search-result {
|
|
||||||
background: lavender;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
.search {
|
|
||||||
.main-search-bar {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #999;
|
|
||||||
border-radius: 0;
|
|
||||||
outline: 0;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Library
|
|
||||||
.header-area {
|
|
||||||
width: 100%;
|
|
||||||
padding: 25px 0 15px 0;
|
|
||||||
position: sticky;
|
|
||||||
z-index:9999;
|
|
||||||
background: #fffffc;
|
|
||||||
top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.library {
|
|
||||||
.table-controls {
|
|
||||||
background: #fffffc;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: sticky;
|
|
||||||
top: 126px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
.pagination {
|
|
||||||
margin: 0;
|
|
||||||
background: #fffffc;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: separate;
|
|
||||||
width: 100%;
|
|
||||||
thead {
|
|
||||||
position: sticky;
|
|
||||||
top: 250px;
|
|
||||||
z-index: 1;
|
|
||||||
background: #fffffc;
|
|
||||||
min-height: 130px;
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
td {
|
|
||||||
border: 0 none;
|
|
||||||
.card {
|
|
||||||
margin: 8px 0 7px 0;
|
|
||||||
.name {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
padding: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comic Detail
|
|
||||||
.control-palette {
|
|
||||||
background-color: #fff6de;
|
|
||||||
display: inline-block;
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
// padding: 1.5rem 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// airdcpp downloads tab
|
|
||||||
.tabs {
|
|
||||||
.download-icon-labels {
|
|
||||||
.downloads-count {
|
|
||||||
margin: 0 1em -1px 0.4em;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.download-tab-name {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawer content padding override
|
|
||||||
.slide-pane__content {
|
|
||||||
padding: 24px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-pane__header {
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comic-vine-match-drawer {
|
|
||||||
// comic detail drawer
|
|
||||||
|
|
||||||
.search-criteria-card {
|
|
||||||
width: 100%;
|
|
||||||
.card-content {
|
|
||||||
padding: 10px;
|
|
||||||
.ant-divider-horizontal {
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
margin: 5px 0 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume detail
|
|
||||||
.volume-details {
|
|
||||||
.is-volume-related {
|
|
||||||
$tag-background-color: $volume-color;
|
|
||||||
}
|
|
||||||
.issues-container {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -10px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
.issues-column {
|
|
||||||
max-width: 102px;
|
|
||||||
margin: 10px;
|
|
||||||
background-clip: padding-box;
|
|
||||||
& > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Potential issue matches in library slideout panel
|
|
||||||
.potential-matches-container {
|
|
||||||
.potential-issue-match {
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
background-color: beige;
|
|
||||||
padding: 10px;
|
|
||||||
pre {
|
|
||||||
padding: 5px;
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
white-space: pre-wrap; /* Since CSS 2.1 */
|
|
||||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
|
||||||
white-space: -pre-wrap; /* Opera 4-6 */
|
|
||||||
white-space: -o-pre-wrap; /* Opera 7 */
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.generic-card {
|
|
||||||
max-width: 90px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// comicvine search results
|
|
||||||
.search-results-container {
|
|
||||||
margin: 15px 0 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.search-result {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f2f1f9;
|
|
||||||
.cover-image {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.search-result-details {
|
|
||||||
.score {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.volume-information {
|
|
||||||
margin-top: -2.5em;
|
|
||||||
width: 80%;
|
|
||||||
background: #fdecd1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.vertical-line {
|
|
||||||
position: relative;
|
|
||||||
top: -25px;
|
|
||||||
left: 1.5rem;
|
|
||||||
border: 2px dotted #ccc;
|
|
||||||
width: 20px;
|
|
||||||
min-height: 35px;
|
|
||||||
|
|
||||||
border-color: transparent transparent #f3a22d #f3a22d;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Library grid
|
|
||||||
.my-masonry-grid {
|
|
||||||
display: -webkit-box; /* Not needed if autoprefixing */
|
|
||||||
display: -ms-flexbox; /* Not needed if autoprefixing */
|
|
||||||
display: flex;
|
|
||||||
margin-left: -30px; /* gutter size offset */
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.my-masonry-grid_column {
|
|
||||||
padding-left: 30px; /* gutter size */
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-masonry-grid_column > div {
|
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// progress
|
|
||||||
.progress-indicator-container {
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.indicator {
|
|
||||||
padding: 5px;
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import React, { ReactElement, useEffect, useState, useContext } from "react";
|
|
||||||
import { Form, Field } from "react-final-form";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
|
||||||
import Select from "react-select";
|
|
||||||
import { saveSettings } from "../../actions/settings.actions";
|
|
||||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
|
||||||
|
|
||||||
export const AirDCPPHubsForm = (airDCPPClientUserSettings): ReactElement => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [hubList, setHubList] = useState([]);
|
|
||||||
const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
|
||||||
const {
|
|
||||||
airDCPPState: { settings, socket },
|
|
||||||
} = airDCPPConfiguration;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!isEmpty(settings)) {
|
|
||||||
const hubs = await socket.get(`hubs`);
|
|
||||||
const hubSelectionOptions = hubs.map(({ hub_url, identity }) => ({
|
|
||||||
value: hub_url,
|
|
||||||
label: identity.name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setHubList(hubSelectionOptions);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSubmit = (values) => {
|
|
||||||
if (!isUndefined(values.hubs)) {
|
|
||||||
dispatch(saveSettings({ ...settings, hubs: values.hubs }, settings._id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = async () => {};
|
|
||||||
|
|
||||||
const SelectAdapter = ({ input, ...rest }) => {
|
|
||||||
return <Select {...input} {...rest} isClearable isMulti />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
validate={validate}
|
|
||||||
render={({ handleSubmit }) => (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<h3 className="title">Hubs</h3>
|
|
||||||
<h6 className="subtitle has-text-grey-light">
|
|
||||||
Select the hubs you want to perform searches against.
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div className="field">
|
|
||||||
<label className="label">AirDC++ Host</label>
|
|
||||||
<div className="control">
|
|
||||||
<Field
|
|
||||||
name="hubs"
|
|
||||||
component={SelectAdapter}
|
|
||||||
className="basic-multi-select"
|
|
||||||
placeholder="Select Hubs to Search Against"
|
|
||||||
options={hubList}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" className="button is-primary">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-4">
|
|
||||||
<article className="message is-warning">
|
|
||||||
<div className="message-body is-size-6 is-family-secondary">
|
|
||||||
Your selection in the dropdown <strong>will replace</strong> the
|
|
||||||
existing selection.
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<div className="box mt-3">
|
|
||||||
<h6>Selected hubs</h6>
|
|
||||||
{settings.directConnect.client.hubs.map(({ value, label }) => (
|
|
||||||
<div key={value}>
|
|
||||||
<div>{label}</div>
|
|
||||||
<span className="is-size-7">{value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AirDCPPHubsForm;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React, { ReactElement } from "react";
|
|
||||||
|
|
||||||
export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
|
|
||||||
const { settings } = settingsObject;
|
|
||||||
console.log(settings);
|
|
||||||
return (
|
|
||||||
<div className="mt-4 is-clearfix">
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-content">
|
|
||||||
<span className="tag is-pulled-right is-primary">Connected</span>
|
|
||||||
<div className="content is-size-7">
|
|
||||||
<dl>
|
|
||||||
<dt>{settings._id}</dt>
|
|
||||||
<dt>Client version: {settings.system_info.client_version}</dt>
|
|
||||||
<dt>Hostname: {settings.system_info.hostname}</dt>
|
|
||||||
<dt>Platform: {settings.system_info.platform}</dt>
|
|
||||||
|
|
||||||
<dt>Username: {settings.user.username}</dt>
|
|
||||||
|
|
||||||
<dt>Active Sessions: {settings.user.active_sessions}</dt>
|
|
||||||
<dt>
|
|
||||||
Permissions:{" "}
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(settings.user.permissions, undefined, 2)}
|
|
||||||
</pre>
|
|
||||||
</dt>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AirDCPPSettingsConfirmation;
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import React, { ReactElement, useCallback, useContext } from "react";
|
|
||||||
import { Form, Field } from "react-final-form";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { saveSettings, deleteSettings } from "../../actions/settings.actions";
|
|
||||||
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
|
|
||||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
|
||||||
import { isUndefined, isEmpty, isNil } from "lodash";
|
|
||||||
|
|
||||||
export const AirDCPPSettingsForm = (): ReactElement => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const airDCPPSettings = useContext(AirDCPPSocketContext);
|
|
||||||
|
|
||||||
const hostValidator = (hostname: string): string | null => {
|
|
||||||
const hostnameRegex = /[\W]+/gm;
|
|
||||||
try {
|
|
||||||
if (!isUndefined(hostname)) {
|
|
||||||
const matches = hostname.match(hostnameRegex);
|
|
||||||
return (isNil(matches) && matches.length !== 0) ? hostname : "Invalid hostname; it should not contain special characters";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = useCallback(async (values) => {
|
|
||||||
try {
|
|
||||||
airDCPPSettings.setSettings(values);
|
|
||||||
dispatch(
|
|
||||||
saveSettings({
|
|
||||||
host: values,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const removeSettings = useCallback(async () => {
|
|
||||||
airDCPPSettings.setSettings({});
|
|
||||||
dispatch(deleteSettings());
|
|
||||||
}, []);
|
|
||||||
const validate = async () => { };
|
|
||||||
const initFormData = !isUndefined(
|
|
||||||
airDCPPSettings.airDCPPState.settings.directConnect,
|
|
||||||
)
|
|
||||||
? airDCPPSettings.airDCPPState.settings.directConnect.client.host
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
validate={validate}
|
|
||||||
initialValues={initFormData}
|
|
||||||
render={({ handleSubmit }) => (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<h2>AirDC++ Connection Information</h2>
|
|
||||||
<label className="label">AirDC++ Hostname</label>
|
|
||||||
<div className="field has-addons">
|
|
||||||
<p className="control">
|
|
||||||
<span className="select">
|
|
||||||
<Field name="protocol" component="select">
|
|
||||||
<option>Protocol</option>
|
|
||||||
<option value="http">http://</option>
|
|
||||||
<option value="https">https://</option>
|
|
||||||
</Field>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div className="control is-expanded">
|
|
||||||
<Field
|
|
||||||
name="hostname"
|
|
||||||
validate={hostValidator}>
|
|
||||||
{({ input, meta }) => (
|
|
||||||
<div>
|
|
||||||
<input {...input} type="text" placeholder="AirDC++ hostname" className="input" />
|
|
||||||
{meta.error && meta.touched && <span className="is-size-7 has-text-danger">{meta.error}</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<p className="control">
|
|
||||||
<Field
|
|
||||||
name="port"
|
|
||||||
component="input"
|
|
||||||
className="input"
|
|
||||||
placeholder="AirDC++ port"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="field">
|
|
||||||
<div className="is-clearfix">
|
|
||||||
<label className="label">Credentials</label>
|
|
||||||
</div>
|
|
||||||
<div className="field-body">
|
|
||||||
<div className="field">
|
|
||||||
<p className="control is-expanded has-icons-left">
|
|
||||||
<Field
|
|
||||||
name="username"
|
|
||||||
component="input"
|
|
||||||
className="input"
|
|
||||||
placeholder="Username"
|
|
||||||
/>
|
|
||||||
<span className="icon is-small is-left">
|
|
||||||
<i className="fa-solid fa-user-ninja"></i>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="field">
|
|
||||||
<p className="control is-expanded has-icons-left has-icons-right">
|
|
||||||
<Field
|
|
||||||
name="password"
|
|
||||||
component="input"
|
|
||||||
type="password"
|
|
||||||
className="input"
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
<span className="icon is-small is-left">
|
|
||||||
<i className="fa-solid fa-lock"></i>
|
|
||||||
</span>
|
|
||||||
<span className="icon is-small is-right">
|
|
||||||
<i className="fas fa-check"></i>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="field is-grouped">
|
|
||||||
<p className="control">
|
|
||||||
<button type="submit" className="button is-primary">
|
|
||||||
{!isEmpty(initFormData) ? "Update" : "Save"}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</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;
|
|
||||||
@@ -1,140 +1,15 @@
|
|||||||
import React, { ReactElement, useContext, useEffect } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import Dashboard from "./Dashboard/Dashboard";
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { Navbar2 } from "./shared/Navbar2";
|
||||||
import Import from "./Import";
|
|
||||||
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
|
|
||||||
import TabulatedContentContainer from "./Library/TabulatedContentContainer";
|
|
||||||
import LibraryGrid from "./Library/LibraryGrid";
|
|
||||||
import Search from "./Search";
|
|
||||||
import Settings from "./Settings";
|
|
||||||
import VolumeDetail from "./VolumeDetail/VolumeDetail";
|
|
||||||
import Downloads from "./Downloads/Downloads";
|
|
||||||
|
|
||||||
import { Routes, Route } from "react-router-dom";
|
|
||||||
import Navbar from "./Navbar";
|
|
||||||
import "../assets/scss/App.scss";
|
import "../assets/scss/App.scss";
|
||||||
import {
|
|
||||||
AirDCPPSocketContextProvider,
|
|
||||||
AirDCPPSocketContext,
|
|
||||||
} from "../context/AirDCPPSocket";
|
|
||||||
import { isEmpty, isUndefined } from "lodash";
|
|
||||||
import {
|
|
||||||
AIRDCPP_DOWNLOAD_PROGRESS_TICK,
|
|
||||||
LS_SINGLE_IMPORT,
|
|
||||||
} from "../constants/action-types";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that initializes an AirDC++ socket connection
|
|
||||||
* 1. Initializes event listeners for download init, tick and complete events
|
|
||||||
* 2. Handles errors in case the connection to AirDC++ is not established or terminated
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
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 => {
|
||||||
return (
|
return (
|
||||||
<AirDCPPSocketContextProvider>
|
<>
|
||||||
<div>
|
<Navbar2 />
|
||||||
<AirDCPPSocketComponent />
|
<Outlet />
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { ReactElement } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { isEmpty, isNil } from "lodash";
|
|
||||||
|
|
||||||
interface ICardProps {
|
|
||||||
orientation: string;
|
|
||||||
imageUrl: string;
|
|
||||||
hasDetails: boolean;
|
|
||||||
title?: PropTypes.ReactElementLike | null;
|
|
||||||
children?: PropTypes.ReactNodeLike;
|
|
||||||
borderColorClass?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
|
||||||
cardContainerStyle?: PropTypes.object;
|
|
||||||
imageStyle?: PropTypes.object;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderCard = (props): ReactElement => {
|
|
||||||
switch (props.orientation) {
|
|
||||||
case "horizontal":
|
|
||||||
return (
|
|
||||||
<div className="card-container">
|
|
||||||
<div className="card generic-card">
|
|
||||||
<div className="is-horizontal">
|
|
||||||
<div className="card-image">
|
|
||||||
<img
|
|
||||||
style={props.imageStyle}
|
|
||||||
src={props.imageUrl}
|
|
||||||
alt="Placeholder image"
|
|
||||||
className="cropped-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{props.hasDetails && (
|
|
||||||
<div className="card-content">{props.children}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "vertical":
|
|
||||||
return (
|
|
||||||
<div onClick={props.onClick}>
|
|
||||||
<div className="generic-card" style={props.cardContainerStyle}>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
!isNil(props.borderColorClass)
|
|
||||||
? `${props.borderColorClass}`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
props.hasDetails
|
|
||||||
? "partial-rounded-card-image"
|
|
||||||
: "rounded-card-image"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<figure>
|
|
||||||
<img
|
|
||||||
src={props.imageUrl}
|
|
||||||
style={props.imageStyle}
|
|
||||||
alt="Placeholder image"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
{props.hasDetails && (
|
|
||||||
<div
|
|
||||||
className="card-content"
|
|
||||||
style={{ backgroundColor: props.backgroundColor }}
|
|
||||||
>
|
|
||||||
{!isNil(props.title) ? (
|
|
||||||
<div className="card-title is-size-8 is-family-secondary">
|
|
||||||
{props.title}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Card = (props: ICardProps): ReactElement => {
|
|
||||||
return renderCard(props);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Card;
|
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import React, {
|
import React, { useCallback, ReactElement, useEffect, useState } from "react";
|
||||||
useCallback,
|
import { getBundlesForComic, sleep } from "../../actions/airdcpp.actions";
|
||||||
useContext,
|
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
|
||||||
ReactElement,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import {
|
|
||||||
search,
|
|
||||||
downloadAirDCPPItem,
|
|
||||||
getBundlesForComic,
|
|
||||||
} from "../../actions/airdcpp.actions";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { RootState, SearchInstance } from "threetwo-ui-typings";
|
import { RootState, SearchInstance } from "threetwo-ui-typings";
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { Form, Field } from "react-final-form";
|
import { Form, Field } from "react-final-form";
|
||||||
|
import { difference } from "../../shared/utils/object.utils";
|
||||||
import { isEmpty, isNil, map } from "lodash";
|
import { isEmpty, isNil, map } from "lodash";
|
||||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
import { useStore } from "../../store";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
interface IAcquisitionPanelProps {
|
interface IAcquisitionPanelProps {
|
||||||
query: any;
|
query: any;
|
||||||
@@ -27,107 +21,249 @@ interface IAcquisitionPanelProps {
|
|||||||
export const AcquisitionPanel = (
|
export const AcquisitionPanel = (
|
||||||
props: IAcquisitionPanelProps,
|
props: IAcquisitionPanelProps,
|
||||||
): ReactElement => {
|
): ReactElement => {
|
||||||
|
const {
|
||||||
|
airDCPPSocketInstance,
|
||||||
|
airDCPPClientConfiguration,
|
||||||
|
airDCPPSessionInformation,
|
||||||
|
airDCPPDownloadTick,
|
||||||
|
} = useStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
||||||
|
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
|
||||||
|
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
||||||
|
airDCPPDownloadTick: state.airDCPPDownloadTick,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SearchData {
|
||||||
|
query: Pick<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
|
||||||
|
hub_urls: string[] | undefined | null;
|
||||||
|
priority: PriorityEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hubs list from an AirDCPP Socket
|
||||||
|
*/
|
||||||
|
const { data: hubs } = useQuery({
|
||||||
|
queryKey: ["hubs"],
|
||||||
|
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
|
||||||
|
});
|
||||||
|
const { comicObjectId } = props;
|
||||||
const issueName = props.query.issue.name || "";
|
const issueName = props.query.issue.name || "";
|
||||||
// const { settings } = props;
|
|
||||||
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
||||||
|
|
||||||
// Selectors for picking state
|
|
||||||
const airDCPPSearchResults = useSelector((state: RootState) => {
|
|
||||||
return state.airdcpp.searchResults;
|
|
||||||
});
|
|
||||||
const isAirDCPPSearchInProgress = useSelector(
|
|
||||||
(state: RootState) => state.airdcpp.isAirDCPPSearchInProgress,
|
|
||||||
);
|
|
||||||
const searchInfo = useSelector(
|
|
||||||
(state: RootState) => state.airdcpp.searchInfo,
|
|
||||||
);
|
|
||||||
const searchInstance: SearchInstance = useSelector(
|
|
||||||
(state: RootState) => state.airdcpp.searchInstance,
|
|
||||||
);
|
|
||||||
|
|
||||||
// const settings = useSelector((state: RootState) => state.settings.data);
|
|
||||||
const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [dcppQuery, setDcppQuery] = useState({});
|
const [dcppQuery, setDcppQuery] = useState({});
|
||||||
|
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState([]);
|
||||||
|
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
|
||||||
|
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState({});
|
||||||
|
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState({});
|
||||||
|
|
||||||
|
// Construct a AirDC++ query based on metadata inferred, upon component mount
|
||||||
|
// Pre-populate the search input with the search string, so that
|
||||||
|
// All the user has to do is hit "Search AirDC++"
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEmpty(airDCPPConfiguration.airDCPPState.settings)) {
|
// AirDC++ search query
|
||||||
// AirDC++ search query
|
const dcppSearchQuery = {
|
||||||
const dcppSearchQuery = {
|
query: {
|
||||||
query: {
|
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
|
||||||
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
|
extensions: ["cbz", "cbr", "cb7"],
|
||||||
extensions: ["cbz", "cbr", "cb7"],
|
},
|
||||||
},
|
hub_urls: map(hubs, (item) => item.value),
|
||||||
hub_urls: map(
|
priority: 5,
|
||||||
airDCPPConfiguration.airDCPPState.settings.directConnect.client.hubs,
|
};
|
||||||
(item) => item.value,
|
setDcppQuery(dcppSearchQuery);
|
||||||
),
|
}, []);
|
||||||
priority: 5,
|
|
||||||
};
|
|
||||||
setDcppQuery(dcppSearchQuery);
|
|
||||||
}
|
|
||||||
}, [airDCPPConfiguration]);
|
|
||||||
|
|
||||||
const getDCPPSearchResults = useCallback(
|
/**
|
||||||
async (searchQuery) => {
|
* Method to perform a search via an AirDC++ websocket
|
||||||
const manualQuery = {
|
* @param {SearchData} data - a SearchData query
|
||||||
query: {
|
* @param {any} ADCPPSocket - an intialized AirDC++ socket instance
|
||||||
pattern: `${searchQuery.issueName}`,
|
*/
|
||||||
extensions: ["cbz", "cbr", "cb7"],
|
const search = async (data: SearchData, ADCPPSocket: any) => {
|
||||||
|
try {
|
||||||
|
if (!ADCPPSocket.isConnected()) {
|
||||||
|
await ADCPPSocket();
|
||||||
|
}
|
||||||
|
const instance: SearchInstance = await ADCPPSocket.post("search");
|
||||||
|
setAirDCPPSearchStatus(true);
|
||||||
|
|
||||||
|
// We want to get notified about every new result in order to make the user experience better
|
||||||
|
await ADCPPSocket.addListener(
|
||||||
|
`search`,
|
||||||
|
"search_result_added",
|
||||||
|
async (groupedResult) => {
|
||||||
|
// ...add the received result in the UI
|
||||||
|
// (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results)
|
||||||
|
setAirDCPPSearchResults((state) => [...state, groupedResult]);
|
||||||
},
|
},
|
||||||
hub_urls: map(
|
instance.id,
|
||||||
airDCPPConfiguration.airDCPPState.settings.directConnect.client.hubs,
|
|
||||||
(item) => item.value,
|
|
||||||
),
|
|
||||||
priority: 5,
|
|
||||||
};
|
|
||||||
dispatch(
|
|
||||||
search(manualQuery, airDCPPConfiguration.airDCPPState.socket, {
|
|
||||||
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
|
|
||||||
password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
},
|
|
||||||
[dispatch, airDCPPConfiguration],
|
// We also want to update the existing items in our list when new hits arrive for the previously listed files/directories
|
||||||
);
|
await ADCPPSocket.addListener(
|
||||||
|
`search`,
|
||||||
|
"search_result_updated",
|
||||||
|
async (groupedResult) => {
|
||||||
|
// ...update properties of the existing result in the UI
|
||||||
|
const bundleToUpdateIndex = airDCPPSearchResults?.findIndex(
|
||||||
|
(bundle) => bundle.result.id === groupedResult.result.id,
|
||||||
|
);
|
||||||
|
const updatedState = [...airDCPPSearchResults];
|
||||||
|
if (
|
||||||
|
!isNil(difference(updatedState[bundleToUpdateIndex], groupedResult))
|
||||||
|
) {
|
||||||
|
updatedState[bundleToUpdateIndex] = groupedResult;
|
||||||
|
}
|
||||||
|
setAirDCPPSearchResults((state) => [...state, ...updatedState]);
|
||||||
|
},
|
||||||
|
instance.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to show something to the user in case the search won't yield any results so that he won't be waiting forever)
|
||||||
|
// Wait for 5 seconds for any results to arrive after the searches were sent to the hubs
|
||||||
|
await ADCPPSocket.addListener(
|
||||||
|
`search`,
|
||||||
|
"search_hub_searches_sent",
|
||||||
|
async (searchInfo) => {
|
||||||
|
await sleep(5000);
|
||||||
|
|
||||||
|
// Check the number of received results (in real use cases we should know that even without calling the API)
|
||||||
|
const currentInstance = await ADCPPSocket.get(
|
||||||
|
`search/${instance.id}`,
|
||||||
|
);
|
||||||
|
setAirDCPPSearchInstance(currentInstance);
|
||||||
|
setAirDCPPSearchInfo(searchInfo);
|
||||||
|
console.log("Asdas", airDCPPSearchInfo);
|
||||||
|
if (currentInstance.result_count === 0) {
|
||||||
|
// ...nothing was received, show an informative message to the user
|
||||||
|
console.log("No more search results.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The search can now be considered to be "complete"
|
||||||
|
// If there's an "in progress" indicator in the UI, that could also be disabled here
|
||||||
|
setAirDCPPSearchInstance(instance);
|
||||||
|
setAirDCPPSearchStatus(false);
|
||||||
|
},
|
||||||
|
instance.id,
|
||||||
|
);
|
||||||
|
// Finally, perform the actual search
|
||||||
|
await ADCPPSocket.post(`search/${instance.id}/hub_search`, data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to download a bundle associated with a search result from AirDC++
|
||||||
|
* @param {Number} searchInstanceId - description
|
||||||
|
* @param {String} resultId - description
|
||||||
|
* @param {String} comicObjectId - description
|
||||||
|
* @param {String} name - description
|
||||||
|
* @param {Number} size - description
|
||||||
|
* @param {any} type - description
|
||||||
|
* @param {any} ADCPPSocket - description
|
||||||
|
* @returns {void} - description
|
||||||
|
*/
|
||||||
|
const download = async (
|
||||||
|
searchInstanceId: Number,
|
||||||
|
resultId: String,
|
||||||
|
comicObjectId: String,
|
||||||
|
name: String,
|
||||||
|
size: Number,
|
||||||
|
type: any,
|
||||||
|
ADCPPSocket: any,
|
||||||
|
): void => {
|
||||||
|
try {
|
||||||
|
if (!ADCPPSocket.isConnected()) {
|
||||||
|
await ADCPPSocket.connect();
|
||||||
|
}
|
||||||
|
let bundleDBImportResult = {};
|
||||||
|
const downloadResult = await ADCPPSocket.post(
|
||||||
|
`search/${searchInstanceId}/results/${resultId}/download`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNil(downloadResult)) {
|
||||||
|
bundleDBImportResult = await axios({
|
||||||
|
method: "POST",
|
||||||
|
url: `http://localhost:3000/api/library/applyAirDCPPDownloadMetadata`,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
bundleId: downloadResult.bundle_info.id,
|
||||||
|
comicObjectId,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// dispatch({
|
||||||
|
// type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
|
||||||
|
// downloadResult,
|
||||||
|
// bundleDBImportResult,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// dispatch({
|
||||||
|
// type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
||||||
|
// comicBookDetail: bundleDBImportResult.data,
|
||||||
|
// IMS_inProgress: false,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getDCPPSearchResults = async (searchQuery) => {
|
||||||
|
const manualQuery = {
|
||||||
|
query: {
|
||||||
|
pattern: `${searchQuery.issueName}`,
|
||||||
|
extensions: ["cbz", "cbr", "cb7"],
|
||||||
|
},
|
||||||
|
hub_urls: map(hubs, (hub) => hub.hub_url),
|
||||||
|
priority: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
search(manualQuery, airDCPPSocketInstance);
|
||||||
|
};
|
||||||
|
|
||||||
// download via AirDC++
|
// download via AirDC++
|
||||||
const downloadDCPPResult = useCallback(
|
const downloadDCPPResult = useCallback(
|
||||||
(searchInstanceId, resultId, name, size, type) => {
|
(searchInstanceId, resultId, name, size, type) => {
|
||||||
dispatch(
|
// dispatch(
|
||||||
downloadAirDCPPItem(
|
// downloadAirDCPPItem(
|
||||||
searchInstanceId,
|
// searchInstanceId,
|
||||||
resultId,
|
// resultId,
|
||||||
props.comicObjectId,
|
// props.comicObjectId,
|
||||||
name,
|
// name,
|
||||||
size,
|
// size,
|
||||||
type,
|
// type,
|
||||||
airDCPPConfiguration.airDCPPState.socket,
|
// airDCPPConfiguration.airDCPPState.socket,
|
||||||
{
|
// {
|
||||||
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
|
// username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
|
||||||
password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`,
|
// password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`,
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
// this is to update the download count badge on the downloads tab
|
// this is to update the download count badge on the downloads tab
|
||||||
dispatch(
|
// dispatch(
|
||||||
getBundlesForComic(
|
// getBundlesForComic(
|
||||||
props.comicObjectId,
|
// props.comicObjectId,
|
||||||
airDCPPConfiguration.airDCPPState.socket,
|
// airDCPPConfiguration.airDCPPState.socket,
|
||||||
{
|
// {
|
||||||
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
|
// username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
|
||||||
password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`,
|
// password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`,
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
},
|
},
|
||||||
[airDCPPConfiguration],
|
[],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="comic-detail columns">
|
<div className="comic-detail columns">
|
||||||
{!isEmpty(airDCPPConfiguration.airDCPPState.socket) ? (
|
{!isEmpty(airDCPPSocketInstance) ? (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={getDCPPSearchResults}
|
onSubmit={getDCPPSearchResults}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@@ -149,9 +285,7 @@ export const AcquisitionPanel = (
|
|||||||
className="input main-search-bar is-medium"
|
className="input main-search-bar is-medium"
|
||||||
placeholder="Type an issue/volume name"
|
placeholder="Type an issue/volume name"
|
||||||
/>
|
/>
|
||||||
<span className="help is-clearfix is-light is-info">
|
<span className="help is-clearfix is-light is-info"></span>
|
||||||
Use this to perform a manual search.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -161,13 +295,13 @@ export const AcquisitionPanel = (
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={
|
className={
|
||||||
isAirDCPPSearchInProgress
|
airDCPPSearchStatus
|
||||||
? "button is-loading is-warning"
|
? "button is-loading is-warning"
|
||||||
: "button"
|
: "button is-success is-light"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="icon is-small">
|
<span className="icon is-small">
|
||||||
<img src="/img/airdcpp_logo.svg" />
|
<img src="/src/client/assets/img/airdcpp_logo.svg" />
|
||||||
</span>
|
</span>
|
||||||
<span className="airdcpp-text">Search on AirDC++</span>
|
<span className="airdcpp-text">Search on AirDC++</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -182,7 +316,7 @@ export const AcquisitionPanel = (
|
|||||||
<article className="message is-info">
|
<article className="message is-info">
|
||||||
<div className="message-body is-size-6 is-family-secondary">
|
<div className="message-body is-size-6 is-family-secondary">
|
||||||
AirDC++ is not configured. Please configure it in{" "}
|
AirDC++ is not configured. Please configure it in{" "}
|
||||||
<code>Settings</code>.
|
<code>Settings > AirDC++ > Connection</code>.
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,48 +324,58 @@ export const AcquisitionPanel = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AirDC++ search instance details */}
|
{/* AirDC++ search instance details */}
|
||||||
{!isNil(searchInfo) && !isNil(searchInstance) && (
|
{!isNil(airDCPPSearchInstance) &&
|
||||||
<div className="columns">
|
!isEmpty(airDCPPSearchInfo) &&
|
||||||
<div className="column is-one-quarter is-size-7">
|
!isNil(hubs) && (
|
||||||
<div className="card">
|
<div className="columns">
|
||||||
<div className="card-content">
|
<div className="column is-one-quarter is-size-7">
|
||||||
<dl>
|
<div className="card">
|
||||||
<dt>
|
<div className="card-content">
|
||||||
<div className="tags mb-1">
|
<dl>
|
||||||
{airDCPPConfiguration.airDCPPState.settings.directConnect.client.hubs.map(
|
<dt>
|
||||||
({ value }) => (
|
<div className="tags mb-1">
|
||||||
<span className="tag is-warning" key={value}>
|
{hubs.map((value, idx) => (
|
||||||
{value}
|
<span className="tag is-warning" key={idx}>
|
||||||
|
{value.identity.name}
|
||||||
</span>
|
</span>
|
||||||
),
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</dt>
|
||||||
</dt>
|
<dt>
|
||||||
<dt>
|
Query:
|
||||||
Query:{" "}
|
<span className="has-text-weight-semibold">
|
||||||
<span className="has-text-weight-semibold">
|
{airDCPPSearchInfo.query.pattern}
|
||||||
{searchInfo.query.pattern}
|
</span>
|
||||||
</span>
|
</dt>
|
||||||
</dt>
|
<dd>
|
||||||
<dd>Extensions: {searchInfo.query.extensions.join(", ")}</dd>
|
Extensions:
|
||||||
<dd>File type: {searchInfo.query.file_type}</dd>
|
<span className="has-text-weight-semibold">
|
||||||
</dl>
|
{airDCPPSearchInfo.query.extensions.join(", ")}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
<dd>
|
||||||
|
File type:
|
||||||
|
<span className="has-text-weight-semibold">
|
||||||
|
{airDCPPSearchInfo.query.file_type}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-one-quarter is-size-7">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-content">
|
||||||
|
<dl>
|
||||||
|
<dt>Search Instance: {airDCPPSearchInstance.id}</dt>
|
||||||
|
<dt>Owned by {airDCPPSearchInstance.owner}</dt>
|
||||||
|
<dd>Expires in: {airDCPPSearchInstance.expires_in}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="column is-one-quarter is-size-7">
|
)}
|
||||||
<div className="card">
|
|
||||||
<div className="card-content">
|
|
||||||
<dl>
|
|
||||||
<dt>Search Instance: {searchInstance.id}</dt>
|
|
||||||
<dt>Owned by {searchInstance.owner}</dt>
|
|
||||||
<dd>Expires in: {searchInstance.expires_in}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* AirDC++ results */}
|
{/* AirDC++ results */}
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
@@ -259,7 +403,7 @@ export const AcquisitionPanel = (
|
|||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
{result.type.id === "directory" ? (
|
{result.type.id === "directory" ? (
|
||||||
<i className="fas fa-folder"></i>
|
<i className="fas fa-folder"></i>
|
||||||
) : null}{" "}
|
) : null}
|
||||||
{ellipsize(result.name, 70)}
|
{ellipsize(result.name, 70)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -299,19 +443,25 @@ export const AcquisitionPanel = (
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<button
|
||||||
|
className="button is-small is-light is-success"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadDCPPResult(
|
download(
|
||||||
searchInstance.id,
|
airDCPPSearchInstance.id,
|
||||||
result.id,
|
result.id,
|
||||||
|
comicObjectId,
|
||||||
result.name,
|
result.name,
|
||||||
result.size,
|
result.size,
|
||||||
result.type,
|
result.type,
|
||||||
|
airDCPPSocketInstance,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="fas fa-file-download"></i>
|
<span className="icon">
|
||||||
</a>
|
<i className="fas fa-file-download"></i>
|
||||||
|
</span>
|
||||||
|
<span>Download </span>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -323,10 +473,22 @@ export const AcquisitionPanel = (
|
|||||||
<div className="column is-three-fifths">
|
<div className="column is-three-fifths">
|
||||||
<article className="message is-info">
|
<article className="message is-info">
|
||||||
<div className="message-body is-size-6 is-family-secondary">
|
<div className="message-body is-size-6 is-family-secondary">
|
||||||
Searching via <strong>AirDC++</strong> is still in{" "}
|
<p>
|
||||||
<strong>alpha</strong>. Some searches may take arbitrarily long,
|
The default search term is an auto-detected title; you may
|
||||||
or may not work at all. Searches from <code>ADCS</code> hubs are
|
need to change it to get better matches if the auto-detected
|
||||||
more reliable than <code>NMDCS</code> ones.
|
one doesn't work.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="message is-warning">
|
||||||
|
<div className="message-body is-size-6 is-family-secondary">
|
||||||
|
<p className="content">
|
||||||
|
Searching via <strong>AirDC++</strong> is still in
|
||||||
|
<strong>alpha</strong>. Some searches may take arbitrarily
|
||||||
|
long, or may not work at all. Searches from <code>ADCS</code>
|
||||||
|
hubs are more reliable than <code>NMDCS</code> ones.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,4 +498,4 @@ export const AcquisitionPanel = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AcquisitionPanel;
|
export default AcquisitionPanel;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { filter, isEmpty, isNil, isUndefined } from "lodash";
|
import { filter, isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import React, { ReactElement, useCallback } from "react";
|
import React, { ReactElement, useCallback } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import Select, { components } from "react-select";
|
import Select, { components } from "react-select";
|
||||||
import { fetchComicVineMatches } from "../../../actions/fileops.actions";
|
import { fetchComicVineMatches } from "../../../actions/fileops.actions";
|
||||||
import { refineQuery } from "filename-parser";
|
import { refineQuery } from "filename-parser";
|
||||||
@@ -8,7 +7,6 @@ import { refineQuery } from "filename-parser";
|
|||||||
export const Menu = (props): ReactElement => {
|
export const Menu = (props): ReactElement => {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
const { setSlidingPanelContentId, setVisible } = props.handlers;
|
const { setSlidingPanelContentId, setVisible } = props.handlers;
|
||||||
const dispatch = useDispatch();
|
|
||||||
const openDrawerWithCVMatches = useCallback(() => {
|
const openDrawerWithCVMatches = useCallback(() => {
|
||||||
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
|
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
|
||||||
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
|
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
|
||||||
@@ -18,10 +16,10 @@ export const Menu = (props): ReactElement => {
|
|||||||
} else if (!isEmpty(data.sourcedMetadata)) {
|
} else if (!isEmpty(data.sourcedMetadata)) {
|
||||||
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
|
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
|
||||||
}
|
}
|
||||||
dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery));
|
// dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery));
|
||||||
setSlidingPanelContentId("CVMatches");
|
setSlidingPanelContentId("CVMatches");
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}, [dispatch, data]);
|
}, [data]);
|
||||||
|
|
||||||
const openEditMetadataPanel = useCallback(() => {
|
const openEditMetadataPanel = useCallback(() => {
|
||||||
setSlidingPanelContentId("editComicBookMetadata");
|
setSlidingPanelContentId("editComicBookMetadata");
|
||||||
@@ -71,17 +69,29 @@ export const Menu = (props): ReactElement => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const customStyles = {
|
||||||
|
option: (base, { data, isDisabled, isFocused, isSelected }) => {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
backgroundColor: isFocused ? "gray" : "black",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
control: (base) => ({
|
||||||
|
...base,
|
||||||
|
backgroundColor: "black",
|
||||||
|
border: "1px solid #CCC",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
className="basic-single"
|
|
||||||
classNamePrefix="select"
|
|
||||||
components={{ Placeholder }}
|
components={{ Placeholder }}
|
||||||
placeholder={
|
placeholder={
|
||||||
<span>
|
<span>
|
||||||
<i className="fa-solid fa-list"></i> Actions
|
<i className="fa-solid fa-list"></i> Actions
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
styles={customStyles}
|
||||||
name="actions"
|
name="actions"
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
options={filteredActionOptions}
|
options={filteredActionOptions}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, { useState, ReactElement, useCallback } from "react";
|
import React, { useState, ReactElement, useCallback } from "react";
|
||||||
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";
|
||||||
@@ -47,6 +46,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
rawFileDetails,
|
rawFileDetails,
|
||||||
inferredMetadata,
|
inferredMetadata,
|
||||||
sourcedMetadata: { comicvine, locg, comicInfo },
|
sourcedMetadata: { comicvine, locg, comicInfo },
|
||||||
|
acquisition,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
},
|
},
|
||||||
userSettings,
|
userSettings,
|
||||||
} = data;
|
} = data;
|
||||||
@@ -55,34 +57,34 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
|
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
|
||||||
const [modalIsOpen, setIsOpen] = useState(false);
|
const [modalIsOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const comicVineSearchResults = useSelector(
|
// const comicVineSearchResults = useSelector(
|
||||||
(state: RootState) => state.comicInfo.searchResults,
|
// (state: RootState) => state.comicInfo.searchResults,
|
||||||
);
|
// );
|
||||||
const comicVineSearchQueryObject = useSelector(
|
// const comicVineSearchQueryObject = useSelector(
|
||||||
(state: RootState) => state.comicInfo.searchQuery,
|
// (state: RootState) => state.comicInfo.searchQuery,
|
||||||
);
|
// );
|
||||||
const comicVineAPICallProgress = useSelector(
|
// const comicVineAPICallProgress = useSelector(
|
||||||
(state: RootState) => state.comicInfo.inProgress,
|
// (state: RootState) => state.comicInfo.inProgress,
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
const extractedComicBook = useSelector(
|
// const extractedComicBook = useSelector(
|
||||||
(state: RootState) => state.fileOps.extractedComicBookArchive.reading,
|
// (state: RootState) => state.fileOps.extractedComicBookArchive.reading,
|
||||||
);
|
// );
|
||||||
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
|
|
||||||
const openModal = useCallback((filePath) => {
|
const openModal = useCallback((filePath) => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
dispatch(
|
// dispatch(
|
||||||
extractComicArchive(filePath, {
|
// extractComicArchive(filePath, {
|
||||||
type: "full",
|
// type: "full",
|
||||||
purpose: "reading",
|
// purpose: "reading",
|
||||||
imageResizeOptions: {
|
// imageResizeOptions: {
|
||||||
baseWidth: 1024,
|
// baseWidth: 1024,
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const afterOpenModal = useCallback((things) => {
|
const afterOpenModal = useCallback((things) => {
|
||||||
@@ -100,7 +102,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
CVMatches: {
|
CVMatches: {
|
||||||
content: (props) => (
|
content: (props) => (
|
||||||
<>
|
<>
|
||||||
<div className="card search-criteria-card">
|
{/* <div className="card search-criteria-card">
|
||||||
<div className="card-content">
|
<div className="card-content">
|
||||||
<ComicVineSearchForm data={rawFileDetails} />
|
<ComicVineSearchForm data={rawFileDetails} />
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +136,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -193,13 +195,15 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
id: 3,
|
id: 3,
|
||||||
icon: <i className="fa-regular fa-file-archive"></i>,
|
icon: <i className="fa-regular fa-file-archive"></i>,
|
||||||
name: "Archive Operations",
|
name: "Archive Operations",
|
||||||
content: <ArchiveOperations data={data.data} key={3} />,
|
content: <></>,
|
||||||
|
/*
|
||||||
|
<ArchiveOperations data={data.data} key={3} /> */
|
||||||
shouldShow: areRawFileDetailsAvailable,
|
shouldShow: areRawFileDetailsAvailable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 +217,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>
|
||||||
@@ -237,69 +248,65 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
// 2. from the CV-scraped version
|
// 2. from the CV-scraped version
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<section className="container mx-auto">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{!isNil(data) && !isEmpty(data) && (
|
{!isNil(data) && !isEmpty(data) && (
|
||||||
<>
|
<>
|
||||||
<h1 className="title">{issueName}</h1>
|
<div>
|
||||||
<div className="columns is-multiline">
|
<div className="flex flex-row mt-5">
|
||||||
<div className="column is-narrow">
|
|
||||||
<Card
|
<Card
|
||||||
imageUrl={url}
|
imageUrl={url}
|
||||||
orientation={"vertical"}
|
orientation={"cover-only"}
|
||||||
hasDetails={false}
|
hasDetails={false}
|
||||||
cardContainerStyle={{ maxWidth: 275 }}
|
|
||||||
/>
|
/>
|
||||||
{/* action dropdown */}
|
|
||||||
<div className="mt-4 is-size-7">
|
{/* raw file details */}
|
||||||
<Menu
|
|
||||||
data={data.data}
|
|
||||||
handlers={{ setSlidingPanelContentId, setVisible }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* raw file details */}
|
|
||||||
<div className="column">
|
|
||||||
{!isUndefined(rawFileDetails) &&
|
{!isUndefined(rawFileDetails) &&
|
||||||
!isEmpty(rawFileDetails.cover) && (
|
!isEmpty(rawFileDetails.cover) && (
|
||||||
<>
|
<div className="grid">
|
||||||
<RawFileDetails
|
<RawFileDetails
|
||||||
data={{
|
data={{
|
||||||
rawFileDetails: rawFileDetails,
|
rawFileDetails: rawFileDetails,
|
||||||
inferredMetadata: inferredMetadata,
|
inferredMetadata: inferredMetadata,
|
||||||
|
created_at: createdAt,
|
||||||
|
updated_at: updatedAt,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Read comic button */}
|
|
||||||
<button
|
|
||||||
className="button is-success is-light"
|
|
||||||
onClick={() => openModal(rawFileDetails.filePath)}
|
|
||||||
>
|
|
||||||
<i className="fa-solid fa-book-open mr-2"></i>
|
|
||||||
Read
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Modal
|
{/* <Modal
|
||||||
style={{ content: { marginTop: "2rem" } }}
|
style={{ content: { marginTop: "2rem" } }}
|
||||||
isOpen={modalIsOpen}
|
isOpen={modalIsOpen}
|
||||||
onAfterOpen={afterOpenModal}
|
onAfterOpen={afterOpenModal}
|
||||||
onRequestClose={closeModal}
|
onRequestClose={closeModal}
|
||||||
contentLabel="Example Modal"
|
contentLabel="Example Modal"
|
||||||
>
|
>
|
||||||
<button onClick={closeModal}>close</button>
|
<button onClick={closeModal}>close</button>
|
||||||
{extractedComicBook && (
|
{extractedComicBook && (
|
||||||
<ComicViewer
|
<ComicViewer
|
||||||
pages={extractedComicBook}
|
pages={extractedComicBook}
|
||||||
direction="ltr"
|
direction="ltr"
|
||||||
className={{closeButton: "border: 1px solid red;"}}
|
className={{
|
||||||
/>
|
closeButton: "border: 1px solid red;",
|
||||||
)}
|
}}
|
||||||
</Modal>
|
/>
|
||||||
</>
|
)}
|
||||||
|
</Modal> */}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* action dropdown */}
|
||||||
|
{/* <div className="mt-4 is-size-7">
|
||||||
|
<Menu
|
||||||
|
data={data.data}
|
||||||
|
handlers={{ setSlidingPanelContentId, setVisible }}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{<TabControls filteredTabs={filteredTabs} />}
|
<TabControls
|
||||||
|
filteredTabs={filteredTabs}
|
||||||
|
acquisition={acquisition}
|
||||||
|
/>
|
||||||
|
|
||||||
<SlidingPane
|
<SlidingPane
|
||||||
isOpen={visible}
|
isOpen={visible}
|
||||||
@@ -317,4 +324,4 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComicDetail;
|
export default ComicDetail;
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import React, { ReactElement, useContext, useEffect, useState } from "react";
|
import React, { ReactElement, useContext, useEffect, useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { getComicBookDetailById } from "../../actions/comicinfo.actions";
|
import { getComicBookDetailById } from "../../actions/comicinfo.actions";
|
||||||
import { ComicDetail } from "../ComicDetail/ComicDetail";
|
import { ComicDetail } from "../ComicDetail/ComicDetail";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export const ComicDetailContainer = (): ReactElement | null => {
|
export const ComicDetailContainer = (): ReactElement | null => {
|
||||||
const comicBookDetailData = useSelector(
|
|
||||||
(state: RootState) => state.comicInfo.comicBookDetail,
|
|
||||||
);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
||||||
|
const {
|
||||||
|
data: comicBookDetailData,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: [],
|
||||||
|
queryFn: async () =>
|
||||||
|
await axios({
|
||||||
|
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
id: comicObjectId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
console.log(comicBookDetailData);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getComicBookDetailById(comicObjectId));
|
// dispatch(getComicBookDetailById(comicObjectId));
|
||||||
// dispatch(getSettings());
|
// dispatch(getSettings());
|
||||||
}, [dispatch]);
|
}, []);
|
||||||
return !isEmpty(comicBookDetailData) ? (
|
|
||||||
<ComicDetail data={comicBookDetailData} />
|
{
|
||||||
) : null;
|
isError && <>Error</>;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isLoading && <>Loading...</>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
comicBookDetailData?.data && <ComicDetail data={comicBookDetailData.data} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,8 +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";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for performing search against ComicVine
|
* Component for performing search against ComicVine
|
||||||
@@ -14,7 +13,6 @@ import { useDispatch } from "react-redux";
|
|||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
export const ComicVineSearchForm = (data) => {
|
export const ComicVineSearchForm = (data) => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const onSubmit = useCallback((value) => {
|
const onSubmit = useCallback((value) => {
|
||||||
const userInititatedQuery = {
|
const userInititatedQuery = {
|
||||||
inferredIssueDetails: {
|
inferredIssueDetails: {
|
||||||
@@ -24,7 +22,7 @@ export const ComicVineSearchForm = (data) => {
|
|||||||
year: value.issueYear,
|
year: value.issueYear,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
dispatch(fetchComicVineMatches(data, userInititatedQuery));
|
// dispatch(fetchComicVineMatches(data, userInititatedQuery));
|
||||||
}, []);
|
}, []);
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
return true;
|
return true;
|
||||||
@@ -3,10 +3,10 @@ import React, { ReactElement } from "react";
|
|||||||
|
|
||||||
export const DownloadProgressTick = (props): ReactElement => {
|
export const DownloadProgressTick = (props): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<div >
|
<div>
|
||||||
<h4 className="is-size-6">{props.data.name}</h4>
|
<h4 className="is-size-5">{props.data.name}</h4>
|
||||||
<div>
|
<div>
|
||||||
<span className="is-size-3 has-text-weight-semibold">
|
<span className="is-size-4 has-text-weight-semibold">
|
||||||
{prettyBytes(props.data.downloaded_bytes)} of{" "}
|
{prettyBytes(props.data.downloaded_bytes)} of{" "}
|
||||||
{prettyBytes(props.data.size)}{" "}
|
{prettyBytes(props.data.size)}{" "}
|
||||||
</span>
|
</span>
|
||||||
@@ -20,13 +20,12 @@ export const DownloadProgressTick = (props): ReactElement => {
|
|||||||
%
|
%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div className="is-size-5">
|
<div className="is-size-6 mt-1 mb-2">
|
||||||
{prettyBytes(props.data.speed)} per second.
|
<p>{prettyBytes(props.data.speed)} per second.</p>
|
||||||
</div>
|
|
||||||
<div className="is-size-5">
|
|
||||||
Time left:
|
Time left:
|
||||||
{Math.round(parseInt(props.data.seconds_left) / 60)}
|
{Math.round(parseInt(props.data.seconds_left) / 60)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>{props.data.target}</div>
|
<div>{props.data.target}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import React, { useEffect, useContext, ReactElement } from "react";
|
import React, { useEffect, useContext, ReactElement } from "react";
|
||||||
import {
|
import { getBundlesForComic } from "../../actions/airdcpp.actions";
|
||||||
getBundlesForComic,
|
|
||||||
} from "../../actions/airdcpp.actions";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { RootState } from "threetwo-ui-typings";
|
import { RootState } from "threetwo-ui-typings";
|
||||||
import { isEmpty, isNil, map } from "lodash";
|
import { isEmpty, isNil, map } from "lodash";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
|
||||||
|
|
||||||
interface IDownloadsPanelProps {
|
interface IDownloadsPanelProps {
|
||||||
data: any;
|
data: any;
|
||||||
@@ -18,34 +14,33 @@ interface IDownloadsPanelProps {
|
|||||||
export const DownloadsPanel = (
|
export const DownloadsPanel = (
|
||||||
props: IDownloadsPanelProps,
|
props: IDownloadsPanelProps,
|
||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
const bundles = useSelector((state: RootState) => {
|
// const bundles = useSelector((state: RootState) => {
|
||||||
return state.airdcpp.bundles;
|
// return state.airdcpp.bundles;
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
// AirDCPP Socket initialization
|
// // AirDCPP Socket initialization
|
||||||
const userSettings = useSelector((state: RootState) => state.settings.data);
|
// const userSettings = useSelector((state: RootState) => state.settings.data);
|
||||||
const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
// const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
airDCPPState: { socket, settings },
|
airDCPPState: { socket, settings },
|
||||||
} = airDCPPConfiguration;
|
} = airDCPPConfiguration;
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
// Fetch the downloaded files and currently-downloading file(s) from AirDC++
|
// Fetch the downloaded files and currently-downloading file(s) from AirDC++
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
if (!isEmpty(userSettings)) {
|
if (!isEmpty(userSettings)) {
|
||||||
dispatch(
|
// dispatch(
|
||||||
getBundlesForComic(props.comicObjectId, socket, {
|
// getBundlesForComic(props.comicObjectId, socket, {
|
||||||
username: `${settings.directConnect.client.host.username}`,
|
// username: `${settings.directConnect.client.host.username}`,
|
||||||
password: `${settings.directConnect.client.host.password}`,
|
// password: `${settings.directConnect.client.host.password}`,
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
}, [dispatch, airDCPPConfiguration]);
|
}, [airDCPPConfiguration]);
|
||||||
|
|
||||||
const Bundles = (props) => {
|
const Bundles = (props) => {
|
||||||
return !isEmpty(props.data) ? (
|
return !isEmpty(props.data) ? (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useState } from "react";
|
import React, { ReactElement, useCallback, useEffect, useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import { Form, Field } from "react-final-form";
|
import { Form, Field } from "react-final-form";
|
||||||
import arrayMutators from "final-form-arrays";
|
import arrayMutators from "final-form-arrays";
|
||||||
import { FieldArray } from "react-final-form-arrays";
|
import { FieldArray } from "react-final-form-arrays";
|
||||||
@@ -9,7 +8,7 @@ import TextareaAutosize from "react-textarea-autosize";
|
|||||||
export const EditMetadataPanel = (props): ReactElement => {
|
export const EditMetadataPanel = (props): ReactElement => {
|
||||||
const validate = async () => {};
|
const validate = async () => {};
|
||||||
const onSubmit = async () => {};
|
const onSubmit = async () => {};
|
||||||
|
|
||||||
const AsyncSelectPaginateAdapter = ({ input, ...rest }) => {
|
const AsyncSelectPaginateAdapter = ({ input, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<AsyncSelectPaginate
|
<AsyncSelectPaginate
|
||||||
@@ -28,10 +27,9 @@ export const EditMetadataPanel = (props): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const rawFileDetails = useSelector(
|
// const rawFileDetails = useSelector(
|
||||||
(state: RootState) => state.comicInfo.comicBookDetail.rawFileDetails.name,
|
// (state: RootState) => state.comicInfo.comicBookDetail.rawFileDetails.name,
|
||||||
);
|
// );
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
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";
|
||||||
|
|
||||||
@@ -15,12 +14,11 @@ const handleBrokenImage = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MatchResult = (props: MatchResultProps) => {
|
export const MatchResult = (props: MatchResultProps) => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const applyCVMatch = useCallback(
|
const applyCVMatch = useCallback(
|
||||||
(match, comicObjectId) => {
|
// (match, comicObjectId) => {
|
||||||
dispatch(applyComicVineMatch(match, comicObjectId));
|
// dispatch(applyComicVineMatch(match, comicObjectId));
|
||||||
},
|
// },
|
||||||
[dispatch],
|
[],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1,71 +1,138 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { isUndefined } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
|
import { format, parseISO } from "date-fns";
|
||||||
|
|
||||||
export const RawFileDetails = (props): ReactElement => {
|
export const RawFileDetails = (props): ReactElement => {
|
||||||
const { rawFileDetails, inferredMetadata } = props.data;
|
const { rawFileDetails, inferredMetadata, created_at, updated_at } =
|
||||||
|
props.data;
|
||||||
|
const PaperClipIcon = () => <></>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="comic-detail raw-file-details column is-three-fifths">
|
<div className="max-w-2xl ml-5">
|
||||||
<dl>
|
<div className="px-4 sm:px-6">
|
||||||
<dt>Raw File Details</dt>
|
<p className="text-gray-500">
|
||||||
<dd className="is-size-7">
|
<span className="text-xl">{rawFileDetails.name}</span>
|
||||||
{rawFileDetails.containedIn +
|
</p>
|
||||||
"/" +
|
</div>
|
||||||
rawFileDetails.name +
|
<div className="px-4 py-5 sm:px-6">
|
||||||
rawFileDetails.extension}
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
|
||||||
</dd>
|
<div className="sm:col-span-1">
|
||||||
<dd>
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
<div className="field is-grouped mt-2">
|
Raw File Details
|
||||||
<div className="control">
|
</dt>
|
||||||
<div className="tags has-addons">
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
<span className="tag">Size</span>
|
{rawFileDetails.containedIn +
|
||||||
<span className="tag is-info is-light">
|
"/" +
|
||||||
|
rawFileDetails.name +
|
||||||
|
rawFileDetails.extension}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
|
Inferred Issue Metadata
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
Series Name: {inferredMetadata.issue.name}
|
||||||
|
{!isEmpty(inferredMetadata.issue.number) ? (
|
||||||
|
<span className="tag is-primary is-light">
|
||||||
|
{inferredMetadata.issue.number}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">MIMEType</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{/* File extension */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
|
{rawFileDetails.mimeType}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
<dd className="text-sm text-gray-900"></dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-1">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">File Size</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{/* size */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
{prettyBytes(rawFileDetails.fileSize)}
|
{prettyBytes(rawFileDetails.fileSize)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</dd>
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Extension</span>
|
|
||||||
<span className="tag is-primary is-light">
|
|
||||||
{rawFileDetails.extension}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
<div className="sm:col-span-2">
|
||||||
</dl>
|
<dt className="text-sm font-medium text-gray-500">
|
||||||
</div>
|
Import Details
|
||||||
|
</dt>
|
||||||
<div className="content comic-detail raw-file-details mt-3 column is-three-fifths">
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
<dl>
|
{format(parseISO(created_at), "dd MMMM, yyyy")},{" "}
|
||||||
{/* inferred metadata */}
|
{format(parseISO(created_at), "h aaaa")}
|
||||||
<dt>Inferred Issue Metadata</dt>
|
</dd>
|
||||||
<dd>
|
|
||||||
<div className="field is-grouped mt-2">
|
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Name</span>
|
|
||||||
<span className="tag is-info is-light">
|
|
||||||
{inferredMetadata.issue.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isUndefined(inferredMetadata.issue.number) ? (
|
|
||||||
<div className="control">
|
|
||||||
<div className="tags has-addons">
|
|
||||||
<span className="tag">Number</span>
|
|
||||||
<span className="tag is-primary is-light">
|
|
||||||
{inferredMetadata.issue.number}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
<div className="sm:col-span-2">
|
||||||
</dl>
|
<dt className="text-sm font-medium text-gray-500">Attachments</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="divide-y divide-gray-200 rounded-md border border-gray-200"
|
||||||
|
>
|
||||||
|
<li className="flex items-center justify-between py-3 pl-3 pr-4 text-sm">
|
||||||
|
<div className="flex w-0 flex-1 items-center">
|
||||||
|
<PaperClipIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 w-0 flex-1 truncate">
|
||||||
|
resume_back_end_developer.pdf
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between py-3 pl-3 pr-4 text-sm">
|
||||||
|
<div className="flex w-0 flex-1 items-center">
|
||||||
|
{/* Read comic button */}
|
||||||
|
<button
|
||||||
|
className="button is-success is-light"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-book-open mr-2"></i>
|
||||||
|
Read
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -81,6 +148,7 @@ RawFileDetails.propTypes = {
|
|||||||
fileSize: PropTypes.number,
|
fileSize: PropTypes.number,
|
||||||
path: PropTypes.string,
|
path: PropTypes.string,
|
||||||
extension: PropTypes.string,
|
extension: PropTypes.string,
|
||||||
|
mimeType: PropTypes.string,
|
||||||
cover: PropTypes.shape({
|
cover: PropTypes.shape({
|
||||||
filePath: PropTypes.string,
|
filePath: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
@@ -93,5 +161,7 @@ RawFileDetails.propTypes = {
|
|||||||
subtitle: PropTypes.string,
|
subtitle: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
created_at: PropTypes.string,
|
||||||
|
updated_at: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,46 +1,47 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from "react";
|
import React, { ReactElement, useEffect, useState } from "react";
|
||||||
import { isEmpty, isNil } from "lodash";
|
import { isEmpty, isNil } from "lodash";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
|
|
||||||
export const TabControls = (props): ReactElement => {
|
export const TabControls = (props): ReactElement => {
|
||||||
const comicBookDetailData = useSelector(
|
// const comicBookDetailData = useSelector(
|
||||||
(state: RootState) => state.comicInfo.comicBookDetail,
|
// (state: RootState) => state.comicInfo.comicBookDetail,
|
||||||
);
|
// );
|
||||||
const { filteredTabs } = props;
|
const { filteredTabs, acquisition } = props;
|
||||||
|
|
||||||
const [active, setActive] = useState(filteredTabs[0].id);
|
const [active, setActive] = useState(filteredTabs[0].id);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActive(filteredTabs[0].id);
|
setActive(filteredTabs[0].id);
|
||||||
}, [comicBookDetailData]);
|
}, [acquisition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tabs">
|
<div className="hidden sm:block mt-7">
|
||||||
<ul>
|
<div className="border-b border-gray-200">
|
||||||
{filteredTabs.map(({ id, name, icon }) => (
|
<nav className="-mb-px flex gap-6" aria-label="Tabs">
|
||||||
<li
|
{filteredTabs.map(({ id, name, icon }) => (
|
||||||
key={id}
|
<a
|
||||||
className={id === active ? "is-active" : ""}
|
key={id}
|
||||||
onClick={() => setActive(id)}
|
className="inline-flex shrink-0 items-center gap-2 border-b-2 border-transparent px-1 pb-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
>
|
aria-current="page"
|
||||||
{/* Downloads tab and count badge */}
|
onClick={() => setActive(id)}
|
||||||
<a>
|
>
|
||||||
{id === 5 &&
|
{/* Downloads tab and count badge */}
|
||||||
!isNil(comicBookDetailData.acquisition.directconnect) ? (
|
{/* <a>
|
||||||
|
{id === 6 && !isNil(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}
|
{acquisition.directconnect.downloads.length}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="icon is-small">{icon}</span>
|
<span className="icon is-small">{icon}</span>
|
||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
|
</a> */}
|
||||||
|
{name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
))}
|
||||||
))}
|
</nav>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filteredTabs.map(({ id, content }) => {
|
{filteredTabs.map(({ id, content }) => {
|
||||||
return active === id ? content : null;
|
return active === id ? content : null;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { ReactElement, useCallback, useState } from "react";
|
import React, { ReactElement, useCallback, useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { DnD } from "../../shared/Draggable/DnD";
|
||||||
import { DnD } from "../../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";
|
||||||
@@ -10,28 +9,27 @@ import { Canvas } from "../../shared/Canvas";
|
|||||||
|
|
||||||
export const ArchiveOperations = (props): ReactElement => {
|
export const ArchiveOperations = (props): ReactElement => {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
const isComicBookExtractionInProgress = useSelector(
|
// const isComicBookExtractionInProgress = useSelector(
|
||||||
(state: RootState) => state.fileOps.comicBookExtractionInProgress,
|
// (state: RootState) => state.fileOps.comicBookExtractionInProgress,
|
||||||
);
|
// );
|
||||||
const extractedComicBookArchive = useSelector(
|
// const extractedComicBookArchive = useSelector(
|
||||||
(state: RootState) => state.fileOps.extractedComicBookArchive.analysis,
|
// (state: RootState) => state.fileOps.extractedComicBookArchive.analysis,
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
|
// const imageAnalysisResult = useSelector((state: RootState) => {
|
||||||
|
// return state.fileOps.imageAnalysisResults;
|
||||||
|
// });
|
||||||
|
|
||||||
const imageAnalysisResult = useSelector((state: RootState) => {
|
|
||||||
return state.fileOps.imageAnalysisResults;
|
|
||||||
});
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const unpackComicArchive = useCallback(() => {
|
const unpackComicArchive = useCallback(() => {
|
||||||
dispatch(
|
// dispatch(
|
||||||
extractComicArchive(data.rawFileDetails.filePath, {
|
// extractComicArchive(data.rawFileDetails.filePath, {
|
||||||
type: "full",
|
// type: "full",
|
||||||
purpose: "analysis",
|
// purpose: "analysis",
|
||||||
imageResizeOptions: {
|
// imageResizeOptions: {
|
||||||
baseWidth: 275,
|
// baseWidth: 275,
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// sliding panel config
|
// sliding panel config
|
||||||
@@ -64,7 +62,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
// sliding panel handlers
|
// sliding panel handlers
|
||||||
const openImageAnalysisPanel = useCallback((imageFilePath) => {
|
const openImageAnalysisPanel = useCallback((imageFilePath) => {
|
||||||
setSlidingPanelContentId("imageAnalysis");
|
setSlidingPanelContentId("imageAnalysis");
|
||||||
dispatch(analyzeImage(imageFilePath));
|
// dispatch(analyzeImage(imageFilePath));
|
||||||
setCurrentImage(imageFilePath);
|
setCurrentImage(imageFilePath);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -126,4 +124,4 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArchiveOperations;
|
export default ArchiveOperations;
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ export const ComicInfoXML = (data): ReactElement => {
|
|||||||
<dd>
|
<dd>
|
||||||
<span className="is-size-7">{json.notes[0]}</span>
|
<span className="is-size-7">{json.notes[0]}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dd className="mt-1 mb-1">{json.summary[0]}</dd>
|
<dd className="mt-1 mb-1">
|
||||||
|
{!isUndefined(json.summary) && json.summary[0]}
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,87 +11,180 @@ import {
|
|||||||
getComicBooks,
|
getComicBooks,
|
||||||
} 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, isUndefined } from "lodash";
|
||||||
|
import Header from "../shared/Header";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Card } from "../shared/Carda";
|
||||||
|
import {
|
||||||
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
|
LIBRARY_SERVICE_HOST,
|
||||||
|
} from "../../constants/endpoints";
|
||||||
|
import {
|
||||||
|
determineCoverFile,
|
||||||
|
determineExternalMetadata,
|
||||||
|
} from "../../shared/utils/metadata.utils";
|
||||||
|
|
||||||
export const Dashboard = (): ReactElement => {
|
export const Dashboard = (): ReactElement => {
|
||||||
const dispatch = useDispatch();
|
const { data: recentComics } = useQuery({
|
||||||
useEffect(() => {
|
queryFn: async () =>
|
||||||
dispatch(fetchVolumeGroups());
|
await axios({
|
||||||
dispatch(
|
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBooks`,
|
||||||
getComicBooks({
|
method: "POST",
|
||||||
paginationOptions: {
|
data: {
|
||||||
page: 0,
|
paginationOptions: {
|
||||||
limit: 5,
|
page: 0,
|
||||||
sort: { updatedAt: "-1" },
|
limit: 5,
|
||||||
|
sort: { updatedAt: "-1" },
|
||||||
|
},
|
||||||
|
predicate: { "acquisition.source.wanted": false },
|
||||||
|
comicStatus: "recent",
|
||||||
},
|
},
|
||||||
predicate: { "acquisition.source.wanted": false },
|
|
||||||
comicStatus: "recent",
|
|
||||||
}),
|
}),
|
||||||
);
|
queryKey: ["recentComics"],
|
||||||
dispatch(
|
});
|
||||||
getComicBooks({
|
|
||||||
paginationOptions: {
|
|
||||||
page: 0,
|
|
||||||
limit: 5,
|
|
||||||
sort: { updatedAt: "-1" },
|
|
||||||
},
|
|
||||||
predicate: { "acquisition.source.wanted": true },
|
|
||||||
comicStatus: "wanted",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
dispatch(getLibraryStatistics());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const recentComics = useSelector(
|
console.log("hari om", recentComics);
|
||||||
(state: RootState) => state.fileOps.recentComics
|
// useEffect(() => {
|
||||||
);
|
// dispatch(fetchVolumeGroups());
|
||||||
const wantedComics = useSelector(
|
// dispatch(
|
||||||
(state: RootState) => state.fileOps.wantedComics,
|
// getComicBooks({
|
||||||
);
|
// paginationOptions: {
|
||||||
const volumeGroups = useSelector(
|
// page: 0,
|
||||||
(state: RootState) => state.fileOps.comicVolumeGroups,
|
// limit: 5,
|
||||||
);
|
// sort: { updatedAt: "-1" },
|
||||||
|
// },
|
||||||
const libraryStatistics = useSelector(
|
// predicate: { "acquisition.source.wanted": false },
|
||||||
(state: RootState) => state.comicInfo.libraryStatistics,
|
// comicStatus: "recent",
|
||||||
);
|
// }),
|
||||||
|
// );
|
||||||
|
// dispatch(
|
||||||
|
// getComicBooks({
|
||||||
|
// paginationOptions: {
|
||||||
|
// page: 0,
|
||||||
|
// limit: 5,
|
||||||
|
// sort: { updatedAt: "-1" },
|
||||||
|
// },
|
||||||
|
// predicate: { "acquisition.source.wanted": true },
|
||||||
|
// comicStatus: "wanted",
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
// dispatch(getLibraryStatistics());
|
||||||
|
// }, []);
|
||||||
|
//
|
||||||
|
// const recentComics = useSelector(
|
||||||
|
// (state: RootState) => state.fileOps.recentComics,
|
||||||
|
// );
|
||||||
|
// const wantedComics = useSelector(
|
||||||
|
// (state: RootState) => state.fileOps.wantedComics,
|
||||||
|
// );
|
||||||
|
// const volumeGroups = useSelector(
|
||||||
|
// (state: RootState) => state.fileOps.comicVolumeGroups,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const libraryStatistics = useSelector(
|
||||||
|
// (state: RootState) => state.comicInfo.libraryStatistics,
|
||||||
|
// );
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container mx-auto max-w-full">
|
||||||
<section className="section">
|
<section>
|
||||||
<h1 className="title">Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
|
<div className="grid grid-cols-5 gap-6">
|
||||||
|
{recentComics?.data.docs.map(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
_id,
|
||||||
|
rawFileDetails,
|
||||||
|
sourcedMetadata: { comicvine, comicInfo, locg },
|
||||||
|
inferredMetadata,
|
||||||
|
acquisition: {
|
||||||
|
source: { name },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
idx,
|
||||||
|
) => {
|
||||||
|
const { issueName, url } = determineCoverFile({
|
||||||
|
rawFileDetails,
|
||||||
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
});
|
||||||
|
const { issue, coverURL, icon } = determineExternalMetadata(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const isComicVineMetadataAvailable =
|
||||||
|
!isUndefined(comicvine) &&
|
||||||
|
!isUndefined(comicvine.volumeInformation);
|
||||||
|
|
||||||
{!isEmpty(recentComics) ? (
|
return (
|
||||||
<>
|
<Card
|
||||||
{/* Pull List */}
|
orientation="vertical-2"
|
||||||
<PullList issues={recentComics} />
|
key={idx}
|
||||||
|
imageUrl={`${LIBRARY_SERVICE_HOST}/${rawFileDetails.cover.filePath}`}
|
||||||
|
title={inferredMetadata.issue.name}
|
||||||
|
hasDetails
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<dt className="sr-only">Address</dt>
|
||||||
|
<dd className="text-sm my-1 flex flex-row gap-1">
|
||||||
|
{/* Issue number */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--hashtag-outline]"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900">
|
||||||
|
{inferredMetadata.issue.number}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/* File extension */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--file-bold-duotone] w-4 h-4"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
{/* Stats */}
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
{!isEmpty(libraryStatistics) && (
|
{rawFileDetails.extension}
|
||||||
<LibraryStatistics stats={libraryStatistics} />
|
</span>
|
||||||
)}
|
</span>
|
||||||
{/* Wanted comics */}
|
</dd>
|
||||||
{!isEmpty(wantedComics) && (
|
</div>
|
||||||
<WantedComicsList comics={wantedComics} />
|
|
||||||
)}
|
<div className="flex flex-row items-center gap-1 my-2">
|
||||||
{/* Recent imports */}
|
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
||||||
<RecentlyImported comicBookCovers={recentComics} />
|
{/* ComicInfo.xml presence */}
|
||||||
|
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
||||||
{/* Volumes */}
|
<i className="h-7 w-7 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300"></i>
|
||||||
{!isEmpty(volumeGroups) && (
|
)}
|
||||||
<VolumeGroups volumeGroups={volumeGroups} />
|
{/* ComicVine metadata presence */}
|
||||||
)}
|
{isComicVineMetadataAvailable && (
|
||||||
</>
|
<span className="w-7 h-7">
|
||||||
) : (
|
<img
|
||||||
<ZeroState
|
src="/src/client/assets/img/cvlogo.svg"
|
||||||
header={"Set the source directory"}
|
alt={"ComicVine metadata detected."}
|
||||||
message={
|
/>
|
||||||
"No comics were found! Please point ThreeTwo! to a directory..."
|
</span>
|
||||||
}
|
)}
|
||||||
/>
|
</div>
|
||||||
)}
|
{/* Raw file presence */}
|
||||||
|
{isNil(rawFileDetails) && (
|
||||||
|
<span className="h-6 w-5 sm:shrink-0 sm:items-center sm:gap-2">
|
||||||
|
<i className="icon-[solar--file-corrupted-outline] h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|||||||
@@ -1,6 +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 "../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";
|
||||||
@@ -20,7 +21,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
getWeeklyPullList({
|
getWeeklyPullList({
|
||||||
startDate: "2022-12-25",
|
startDate: "2023-9-9",
|
||||||
pageSize: "15",
|
pageSize: "15",
|
||||||
currentPage: "1",
|
currentPage: "1",
|
||||||
}),
|
}),
|
||||||
@@ -88,12 +89,9 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<h4 className="title is-4">
|
<Header headerContent="Discover"
|
||||||
<i className="fa-solid fa-splotch"></i> Discover
|
subHeaderContent="Pull List aggregated for the week from League Of Comic Geeks"
|
||||||
</h4>
|
iconClassNames="fa-solid fa-binoculars mr-2"/>
|
||||||
<p className="subtitle is-7">
|
|
||||||
Pull List aggregated for the week from League Of Comic Geeks
|
|
||||||
</p>
|
|
||||||
<div className="field is-grouped">
|
<div className="field is-grouped">
|
||||||
{/* select week */}
|
{/* select week */}
|
||||||
<div className="control">
|
<div className="control">
|
||||||
@@ -127,7 +125,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
|||||||
<Slider {...settings} ref={(c) => (sliderRef = c)}>
|
<Slider {...settings} ref={(c) => (sliderRef = c)}>
|
||||||
{!isNil(pullList) &&
|
{!isNil(pullList) &&
|
||||||
pullList &&
|
pullList &&
|
||||||
map(pullList, ({issue}, idx) => {
|
map(pullList, ({ issue }, idx) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -161,4 +159,4 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PullList;
|
export default PullList;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -83,14 +83,14 @@ export const RecentlyImported = ({
|
|||||||
{/* Raw file presence */}
|
{/* Raw file presence */}
|
||||||
{isNil(rawFileDetails) && (
|
{isNil(rawFileDetails) && (
|
||||||
<span className="icon custom-icon is-small has-text-danger mr-2">
|
<span className="icon custom-icon is-small has-text-danger mr-2">
|
||||||
<img src="/img/missing-file.svg" />
|
<img src="/src/client/assets/img/missing-file.svg" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* ComicInfo.xml presence */}
|
{/* ComicInfo.xml presence */}
|
||||||
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
||||||
<span className="icon custom-icon is-small has-text-danger">
|
<span className="icon custom-icon is-small has-text-danger">
|
||||||
<img
|
<img
|
||||||
src="/img/comicinfoxml.svg"
|
src="/src/client/assets/img/comicinfoxml.svg"
|
||||||
alt={"ComicInfo.xml file detected."}
|
alt={"ComicInfo.xml file detected."}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -99,8 +99,8 @@ export const RecentlyImported = ({
|
|||||||
{isComicBookMetadataAvailable && (
|
{isComicBookMetadataAvailable && (
|
||||||
<span className="icon custom-icon">
|
<span className="icon custom-icon">
|
||||||
<img
|
<img
|
||||||
src="/img/cvlogo.svg"
|
src="/src/client/assets/img/cvlogo.svg"
|
||||||
alt={"ComicInfo.xml file detected."}
|
alt={"ComicVine metadata detected."}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -125,7 +125,7 @@ export const RecentlyImported = ({
|
|||||||
<dd className="is-size-9">
|
<dd className="is-size-9">
|
||||||
<dl>
|
<dl>
|
||||||
<span className="icon custom-icon">
|
<span className="icon custom-icon">
|
||||||
<img src={`/img/${icon}`} />
|
<img src={`/src/client/assets/img/${icon}`} />
|
||||||
</span>
|
</span>
|
||||||
</dl>
|
</dl>
|
||||||
<dl>
|
<dl>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -82,12 +82,12 @@ export const WantedComicsList = ({
|
|||||||
{/* comicVine metadata presence */}
|
{/* comicVine metadata presence */}
|
||||||
{isComicBookMetadataAvailable && (
|
{isComicBookMetadataAvailable && (
|
||||||
<span className="icon custom-icon">
|
<span className="icon custom-icon">
|
||||||
<img src="/img/cvlogo.svg" />
|
<img src="/src/client/assets/img/cvlogo.svg" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isEmpty(locg) && (
|
{!isEmpty(locg) && (
|
||||||
<span className="icon custom-icon">
|
<span className="icon custom-icon">
|
||||||
<img src="/img/locglogo.svg" />
|
<img src="/src/client/assets/img/locglogo.svg" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* Issue type */}
|
{/* Issue type */}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React, { ReactElement, useCallback, useContext, useEffect, useState } from "react";
|
import React, { ReactElement, useEffect, useState } from "react";
|
||||||
import { getTransfers } from "../../actions/airdcpp.actions";
|
import { getTransfers } from "../../actions/airdcpp.actions";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
|
||||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import { searchIssue } from "../../actions/fileops.actions";
|
|
||||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||||
import MetadataPanel from "../shared/MetadataPanel";
|
import MetadataPanel from "../shared/MetadataPanel";
|
||||||
|
|
||||||
@@ -12,16 +9,18 @@ interface IDownloadsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Downloads = (props: IDownloadsProps): ReactElement => {
|
export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||||
const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
// const airDCPPConfiguration = useContext(AirDCPPSocketContext);
|
||||||
const {
|
const {
|
||||||
airDCPPState: { settings, socket },
|
airDCPPState: { settings, socket },
|
||||||
} = airDCPPConfiguration;
|
} = airDCPPConfiguration;
|
||||||
const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
|
|
||||||
const airDCPPTransfers = useSelector(
|
// const airDCPPTransfers = useSelector(
|
||||||
(state: RootState) => state.airdcpp.transfers,
|
// (state: RootState) => state.airdcpp.transfers,
|
||||||
);
|
// );
|
||||||
const issueBundles = useSelector((state: RootState) => state.airdcpp.issue_bundles);
|
// const issueBundles = useSelector(
|
||||||
|
// (state: RootState) => state.airdcpp.issue_bundles,
|
||||||
|
// );
|
||||||
const [bundles, setBundles] = useState([]);
|
const [bundles, setBundles] = useState([]);
|
||||||
// Make the call to get all transfers from AirDC++
|
// Make the call to get all transfers from AirDC++
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,62 +37,86 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUndefined(issueBundles)) {
|
if (!isUndefined(issueBundles)) {
|
||||||
const foo = issueBundles.data.map((bundle) => {
|
const foo = issueBundles.data.map((bundle) => {
|
||||||
const { rawFileDetails, inferredMetadata, acquisition: { directconnect: { downloads } }, sourcedMetadata: { locg, comicvine } } = bundle;
|
const {
|
||||||
|
rawFileDetails,
|
||||||
|
inferredMetadata,
|
||||||
|
acquisition: {
|
||||||
|
directconnect: { downloads },
|
||||||
|
},
|
||||||
|
sourcedMetadata: { locg, comicvine },
|
||||||
|
} = bundle;
|
||||||
const { issueName, url } = determineCoverFile({
|
const { issueName, url } = determineCoverFile({
|
||||||
rawFileDetails, comicvine, locg,
|
rawFileDetails,
|
||||||
|
comicvine,
|
||||||
|
locg,
|
||||||
});
|
});
|
||||||
return { ...bundle, issueName, url }
|
return { ...bundle, issueName, url };
|
||||||
|
});
|
||||||
})
|
|
||||||
setBundles(foo);
|
setBundles(foo);
|
||||||
}
|
}
|
||||||
|
}, [issueBundles]);
|
||||||
|
|
||||||
}, [issueBundles])
|
return !isNil(bundles) ? (
|
||||||
|
|
||||||
return !isNil(bundles) ?
|
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h1 className="title">Downloads</h1>
|
<h1 className="title">Downloads</h1>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column is-half">
|
<div className="column is-half">
|
||||||
{bundles.map(bundle => {
|
{bundles.map((bundle, idx) => {
|
||||||
console.log(bundle);
|
console.log(bundle);
|
||||||
return <>
|
return (
|
||||||
<MetadataPanel
|
<div key={idx}>
|
||||||
data={bundle}
|
<MetadataPanel
|
||||||
imageStyle={{ maxWidth: 80 }}
|
data={bundle}
|
||||||
titleStyle={{ fontSize: "0.8rem" }}
|
imageStyle={{ maxWidth: 80 }}
|
||||||
tagsStyle={{ fontSize: "0.7rem" }}
|
titleStyle={{ fontSize: "0.8rem" }}
|
||||||
containerStyle={{
|
tagsStyle={{ fontSize: "0.7rem" }}
|
||||||
maxWidth: 400,
|
containerStyle={{
|
||||||
padding: 0,
|
maxWidth: 400,
|
||||||
margin: "0 0 8px 0",
|
padding: 0,
|
||||||
}} />
|
margin: "0 0 8px 0",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<table className="table is-size-7">
|
<table className="table is-size-7">
|
||||||
<tr>
|
<thead>
|
||||||
<th>Name</th>
|
<tr>
|
||||||
<th>Size</th>
|
<th>Name</th>
|
||||||
<th>Type</th>
|
<th>Size</th>
|
||||||
<th>Bundle ID</th>
|
<th>Type</th>
|
||||||
</tr>
|
<th>Bundle ID</th>
|
||||||
|
</tr>
|
||||||
{bundle.acquisition.directconnect.downloads.map((bundle) => {
|
</thead>
|
||||||
return(<tr>
|
<tbody>
|
||||||
<td>{bundle.name}</td>
|
{bundle.acquisition.directconnect.downloads.map(
|
||||||
<td>{bundle.size}</td>
|
(bundle, idx) => {
|
||||||
<td>{bundle.type.str}</td>
|
return (
|
||||||
<td><span className="tag is-warning">{bundle.bundleId}</span></td>
|
<tr key={idx}>
|
||||||
</tr>)
|
<td>{bundle.name}</td>
|
||||||
})}
|
<td>{bundle.size}</td>
|
||||||
|
<td>{bundle.type.str}</td>
|
||||||
|
<td>
|
||||||
|
<span className="tag is-warning">
|
||||||
|
{bundle.bundleId}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */}
|
||||||
</>
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div> : <div>asd</div>;
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>There are no downloads.</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Downloads;
|
export default Downloads;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -31,7 +31,7 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
|
|||||||
from: 0,
|
from: 0,
|
||||||
},
|
},
|
||||||
type: "volumeName",
|
type: "volumeName",
|
||||||
trigger: "globalSearchBar"
|
trigger: "globalSearchBar",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -51,7 +51,6 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
|
|||||||
<i className="fa-solid fa-magnifying-glass"></i>
|
<i className="fa-solid fa-magnifying-glass"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEmpty(searchResults) ? (
|
{!isEmpty(searchResults) ? (
|
||||||
<div
|
<div
|
||||||
className="columns box is-multiline"
|
className="columns box is-multiline"
|
||||||
@@ -62,9 +61,10 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
|
|||||||
margin: "60px 0 0 350px",
|
margin: "60px 0 0 350px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{map(searchResults, (result) => (
|
{map(searchResults, (result, idx) => (
|
||||||
<MetadataPanel
|
<MetadataPanel
|
||||||
data={result}
|
data={result}
|
||||||
|
key={idx}
|
||||||
imageStyle={{ maxWidth: 70 }}
|
imageStyle={{ maxWidth: 70 }}
|
||||||
titleStyle={{ fontSize: "0.8rem" }}
|
titleStyle={{ fontSize: "0.8rem" }}
|
||||||
tagsStyle={{ fontSize: "0.7rem" }}
|
tagsStyle={{ fontSize: "0.7rem" }}
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import React, { ReactElement, useCallback, useContext, useState } from "react";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import {
|
|
||||||
fetchComicBookMetadata,
|
|
||||||
toggleImportQueueStatus,
|
|
||||||
} from "../actions/fileops.actions";
|
|
||||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
|
||||||
import Loader from "react-loader-spinner";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
matches?: unknown;
|
|
||||||
fetchComicMetadata?: any;
|
|
||||||
path: string;
|
|
||||||
covers?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the average of two numbers.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method is part of the {@link core-library#Statistics | Statistics subsystem}.
|
|
||||||
*
|
|
||||||
* @param x - The first input number
|
|
||||||
* @param y - The second input number
|
|
||||||
* @returns The arithmetic mean of `x` and `y`
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const Import = (props: IProps): ReactElement => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const libraryQueueResults = useSelector(
|
|
||||||
(state: RootState) => state.fileOps.librarySearchResultCount,
|
|
||||||
);
|
|
||||||
const libraryQueueImportStatus = useSelector(
|
|
||||||
(state: RootState) => state.fileOps.IMSCallInProgress,
|
|
||||||
);
|
|
||||||
const [isImportQueuePaused, setImportQueueStatus] = useState(false);
|
|
||||||
const initiateImport = useCallback(() => {
|
|
||||||
if (typeof props.path !== "undefined") {
|
|
||||||
dispatch(fetchComicBookMetadata(props.path));
|
|
||||||
}
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const toggleImport = useCallback(() => {
|
|
||||||
setImportQueueStatus(!isImportQueuePaused);
|
|
||||||
if (isImportQueuePaused === false) {
|
|
||||||
dispatch(toggleImportQueueStatus({ action: "resume" }));
|
|
||||||
} else if (isImportQueuePaused === true) {
|
|
||||||
dispatch(toggleImportQueueStatus({ action: "pause" }));
|
|
||||||
}
|
|
||||||
}, [isImportQueuePaused]);
|
|
||||||
const pauseIconText = (
|
|
||||||
<>
|
|
||||||
<i className="fa-solid fa-pause mr-2"></i> Pause Import
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
const playIconText = (
|
|
||||||
<>
|
|
||||||
<i className="fa-solid fa-play mr-2"></i> Resume Import
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<section className="section is-small">
|
|
||||||
<h1 className="title">Import</h1>
|
|
||||||
<article className="message is-dark">
|
|
||||||
<div className="message-body">
|
|
||||||
<p className="mb-2">
|
|
||||||
<span className="tag is-medium is-info is-light">
|
|
||||||
Import Only
|
|
||||||
</span>
|
|
||||||
will add comics identified from the mapped folder into the local
|
|
||||||
db.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span className="tag is-medium is-info is-light">
|
|
||||||
Import and Tag
|
|
||||||
</span>
|
|
||||||
will scan the ComicVine, shortboxed APIs and import comics from
|
|
||||||
the mapped folder with the additional metadata.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<p className="buttons">
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
libraryQueueImportStatus
|
|
||||||
? "button is-loading is-medium"
|
|
||||||
: "button is-medium"
|
|
||||||
}
|
|
||||||
onClick={initiateImport}
|
|
||||||
>
|
|
||||||
<span className="icon">
|
|
||||||
<i className="fas fa-file-import"></i>
|
|
||||||
</span>
|
|
||||||
<span>Import Only</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button className="button is-medium">
|
|
||||||
<span className="icon">
|
|
||||||
<i className="fas fa-tag"></i>
|
|
||||||
</span>
|
|
||||||
<span>Import and Tag</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<div className="columns is-multiline">
|
|
||||||
<div className="column is-one-fifth">
|
|
||||||
<div className="box control-palette">
|
|
||||||
<span className="is-size-2 has-text-weight-bold">
|
|
||||||
{JSON.stringify(libraryQueueResults, null, 2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="is-half">
|
|
||||||
<div className="content">
|
|
||||||
<div className="control">
|
|
||||||
<button
|
|
||||||
className="button is-warning is-light"
|
|
||||||
onClick={toggleImport}
|
|
||||||
>
|
|
||||||
{!isImportQueuePaused ? pauseIconText : playIconText}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Import;
|
|
||||||
298
src/client/components/Import/Import.tsx
Normal file
298
src/client/components/Import/Import.tsx
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import React, { ReactElement, useCallback, useEffect } from "react";
|
||||||
|
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import Loader from "react-loader-spinner";
|
||||||
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useStore } from "../../store";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
matches?: unknown;
|
||||||
|
fetchComicMetadata?: any;
|
||||||
|
path: string;
|
||||||
|
covers?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to facilitate the import of comics to the ThreeTwo library
|
||||||
|
*
|
||||||
|
* @param x - The first input number
|
||||||
|
* @param y - The second input number
|
||||||
|
* @returns The arithmetic mean of `x` and `y`
|
||||||
|
*
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const Import = (props: IProps): ReactElement => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { importJobQueue, socketIOInstance } = useStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
importJobQueue: state.importJobQueue,
|
||||||
|
socketIOInstance: state.socketIOInstance,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionId = localStorage.getItem("sessionId");
|
||||||
|
const { mutate: initiateImport } = useMutation({
|
||||||
|
mutationFn: async () =>
|
||||||
|
await axios.request({
|
||||||
|
url: `http://localhost:3000/api/library/newImport`,
|
||||||
|
method: "POST",
|
||||||
|
data: { sessionId },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, isError, isLoading } = useQuery({
|
||||||
|
queryKey: ["allImportJobResults"],
|
||||||
|
queryFn: async () =>
|
||||||
|
await axios({
|
||||||
|
method: "GET",
|
||||||
|
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
||||||
|
socketIOInstance.emit(
|
||||||
|
"call",
|
||||||
|
"socket.setQueueStatus",
|
||||||
|
{
|
||||||
|
queueAction,
|
||||||
|
queueStatus,
|
||||||
|
},
|
||||||
|
(data) => console.log(data),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Method to render import job queue pause/resume controls on the UI
|
||||||
|
*
|
||||||
|
* @param status The `string` status (either `"pause"` or `"resume"`)
|
||||||
|
* @returns ReactElement A `<button/>` that toggles queue status
|
||||||
|
* @remarks Sets the global `importJobQueue.status` state upon toggling
|
||||||
|
*/
|
||||||
|
const renderQueueControls = (status: string): ReactElement | null => {
|
||||||
|
switch (status) {
|
||||||
|
case "running":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
onClick={() => {
|
||||||
|
toggleQueue("pause", "paused");
|
||||||
|
importJobQueue.setStatus("paused");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-md">Pause</span>
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<i className="h-5 w-5 icon-[solar--pause-bold]"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "paused":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
onClick={() => {
|
||||||
|
toggleQueue("resume", "running");
|
||||||
|
importJobQueue.setStatus("running");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-md">Resume</span>
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<i className="h-5 w-5 icon-[solar--play-bold]"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "drained":
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
|
Import
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Import comics into the ThreeTwo library.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||||
|
<article
|
||||||
|
role="alert"
|
||||||
|
className="rounded-lg max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Importing will add comics identified from the mapped folder into
|
||||||
|
ThreeTwo's database.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Metadata from ComicInfo.xml, if present, will also be extracted.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This process could take a while, if you have a lot of comics, or
|
||||||
|
are importing over a network connection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div className="my-4">
|
||||||
|
{importJobQueue.status === "drained" ||
|
||||||
|
(importJobQueue.status === undefined && (
|
||||||
|
<button
|
||||||
|
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-5 py-3 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
onClick={() => {
|
||||||
|
initiateImport();
|
||||||
|
importJobQueue.setStatus("running");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-md">Start Import</span>
|
||||||
|
<span className="w-6 h-6">
|
||||||
|
<i className="h-6 w-6 icon-[solar--file-left-bold-duotone]"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity */}
|
||||||
|
{(importJobQueue.status === "running" ||
|
||||||
|
importJobQueue.status === "paused") && (
|
||||||
|
<>
|
||||||
|
<span className="flex items-center my-5 max-w-screen-lg">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
Import Activity
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
<div className="mt-5 flex flex-col gap-4 sm:mt-0 sm:flex-row sm:items-center">
|
||||||
|
<dl className="grid grid-cols-2 gap-4 sm:grid-cols-2">
|
||||||
|
{/* Successful import counts */}
|
||||||
|
<div className="flex flex-col rounded-lg bg-green-100 dark:bg-green-200 px-4 py-6 text-center">
|
||||||
|
<dd className="text-3xl text-green-600 md:text-5xl">
|
||||||
|
{importJobQueue.successfulJobCount}
|
||||||
|
</dd>
|
||||||
|
<dt className="text-lg font-medium text-gray-500">
|
||||||
|
imported
|
||||||
|
</dt>
|
||||||
|
</div>
|
||||||
|
{/* Failed job counts */}
|
||||||
|
<div className="flex flex-col rounded-lg bg-red-100 dark:bg-red-200 px-4 py-6 text-center">
|
||||||
|
<dd className="text-3xl text-red-600 md:text-5xl">
|
||||||
|
{importJobQueue.failedJobCount}
|
||||||
|
</dd>
|
||||||
|
<dt className="text-lg font-medium text-gray-500">
|
||||||
|
failed
|
||||||
|
</dt>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col dark:text-slate-200 text-slate-400">
|
||||||
|
<dd>{renderQueueControls(importJobQueue.status)}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="mt-2 dark:text-slate-200 text-slate-400">
|
||||||
|
Imported: <span>{importJobQueue.mostRecentImport}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Past imports */}
|
||||||
|
{!isLoading && !isEmpty(data?.data) && (
|
||||||
|
<div className="max-w-screen-lg">
|
||||||
|
<span className="flex items-center mt-6">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
Past Imports
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto mt-4 rounded-lg border border-gray-200">
|
||||||
|
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
||||||
|
<thead className="ltr:text-left rtl:text-right">
|
||||||
|
<tr>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Time Started
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Session Id
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Imported
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Failed
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{data?.data.map((jobResult, id) => {
|
||||||
|
return (
|
||||||
|
<tr key={id}>
|
||||||
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
{format(
|
||||||
|
new Date(jobResult.earliestTimestamp),
|
||||||
|
"EEEE, hh:mma, do LLLL Y",
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
<span className="tag is-warning">
|
||||||
|
{jobResult.sessionId}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
<span className="inline-flex items-center justify-center rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--check-circle-line-duotone] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-nowrap text-sm">
|
||||||
|
{jobResult.completedJobs}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
<span className="inline-flex items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-red-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--close-circle-line-duotone] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p className="whitespace-nowrap text-sm">
|
||||||
|
{jobResult.failedJobs}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Import;
|
||||||
@@ -1,48 +1,52 @@
|
|||||||
import React, { useMemo, ReactElement, useCallback, useEffect } from "react";
|
import React, { useMemo, ReactElement, useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import MetadataPanel from "../shared/MetadataPanel";
|
import MetadataPanel from "../shared/MetadataPanel";
|
||||||
import T2Table from "../shared/T2Table";
|
import T2Table from "../shared/T2Table";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { searchIssue } from "../../actions/fileops.actions";
|
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
|
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { format, fromUnixTime, parseISO } from "date-fns";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that tabulates the contents of the user's ThreeTwo Library.
|
* Component that tabulates the contents of the user's ThreeTwo Library.
|
||||||
*
|
*
|
||||||
* @component
|
* @component
|
||||||
* @example
|
* @example
|
||||||
* <Library />
|
* <Library />
|
||||||
*/
|
*/
|
||||||
export const Library = (): ReactElement => {
|
export const Library = (): ReactElement => {
|
||||||
const searchResults = useSelector(
|
// Default page state
|
||||||
(state: RootState) => state.fileOps.libraryComics,
|
// offset: 0
|
||||||
);
|
const [offset, setOffset] = useState(0);
|
||||||
const searchError = useSelector(
|
|
||||||
(state: RootState) => state.fileOps.librarySearchError,
|
|
||||||
);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(
|
|
||||||
searchIssue(
|
|
||||||
{
|
|
||||||
query: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pagination: {
|
|
||||||
size: 15,
|
|
||||||
from: 0,
|
|
||||||
},
|
|
||||||
type: "all",
|
|
||||||
trigger: "libraryPage"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// programatically navigate to comic detail
|
// Method to fetch paginated issues
|
||||||
|
const fetchIssues = async (searchQuery, offset, type) => {
|
||||||
|
let pagination = {
|
||||||
|
size: 15,
|
||||||
|
from: offset,
|
||||||
|
};
|
||||||
|
return await axios({
|
||||||
|
method: "POST",
|
||||||
|
url: "http://localhost:3000/api/search/searchIssue",
|
||||||
|
data: {
|
||||||
|
searchQuery,
|
||||||
|
pagination,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading, isError, isPlaceholderData } = useQuery({
|
||||||
|
queryKey: ["comics", offset],
|
||||||
|
queryFn: () => fetchIssues({}, offset, "all"),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchResults = data?.data;
|
||||||
|
|
||||||
|
// Programmatically navigate to comic detail
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const navigateToComicDetail = (row) => {
|
const navigateToComicDetail = (row) => {
|
||||||
navigate(`/comic/details/${row.original._id}`);
|
navigate(`/comic/details/${row.original._id}`);
|
||||||
@@ -50,102 +54,112 @@ export const Library = (): ReactElement => {
|
|||||||
|
|
||||||
const ComicInfoXML = (value) => {
|
const ComicInfoXML = (value) => {
|
||||||
return value.data ? (
|
return value.data ? (
|
||||||
<div className="comicvine-metadata">
|
<dl className="flex flex-col text-md p-3 ml-4 my-3 rounded-lg dark:bg-yellow-500 bg-yellow-300 w-max">
|
||||||
<dl>
|
{/* Series Name */}
|
||||||
<span className="tags has-addons is-size-7">
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<span className="tag">Series</span>
|
<span className="pr-1 pt-1">
|
||||||
<span className="tag is-warning is-light">
|
<i className="icon-[solar--bookmark-square-minimalistic-bold-duotone] w-5 h-5"></i>
|
||||||
{ellipsize(value.data.series[0], 25)}
|
</span>
|
||||||
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
{ellipsize(value.data.series[0], 45)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row mt-2 gap-2">
|
||||||
|
{/* Pages */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--notebook-minimalistic-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
Pages: {value.data.pagecount[0]}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</dl>
|
{/* Issue number */}
|
||||||
<dl>
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<div className="field is-grouped is-grouped-multiline">
|
<span className="pr-1 pt-1">
|
||||||
<div className="control">
|
<i className="icon-[solar--hashtag-outline] w-3.5 h-3.5"></i>
|
||||||
<span className="tags has-addons is-size-7 mt-2">
|
</span>
|
||||||
<span className="tag">Pages</span>
|
<span className="text-slate-900 dark:text-slate-900">
|
||||||
<span className="tag is-info is-light has-text-weight-bold">
|
{!isNil(value.data.number) && (
|
||||||
{value.data.pagecount[0]}
|
<span>{parseInt(value.data.number[0], 10)}</span>
|
||||||
</span>
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
<div className="control">
|
</dl>
|
||||||
<span className="tags has-addons is-size-7 mt-2">
|
|
||||||
<span className="tag">Issue</span>
|
|
||||||
{!isNil(value.data.number) && (
|
|
||||||
<span className="tag has-text-weight-bold is-success is-light">
|
|
||||||
{parseInt(value.data.number[0], 10)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WantedStatus = ({ value }) => {
|
const columns = useMemo(
|
||||||
return !value ? <span className="tag is-info is-light">Wanted</span> : null;
|
() => [
|
||||||
};
|
{
|
||||||
const columns = useMemo(() => [
|
header: "Comic Metadata",
|
||||||
{
|
footer: 1,
|
||||||
header: "Comic Metadata",
|
columns: [
|
||||||
footer: 1,
|
{
|
||||||
columns: [
|
header: "File Details",
|
||||||
{
|
id: "fileDetails",
|
||||||
header: "File Details",
|
minWidth: 400,
|
||||||
id: "fileDetails",
|
accessorKey: "_source",
|
||||||
minWidth: 400,
|
cell: (info) => {
|
||||||
accessorKey: "_source",
|
return <MetadataPanel data={info.getValue()} />;
|
||||||
cell: info => {
|
},
|
||||||
return <MetadataPanel data={info.getValue()} />;
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
header: "ComicInfo.xml",
|
||||||
header: "ComicInfo.xml",
|
accessorKey: "_source.sourcedMetadata.comicInfo",
|
||||||
accessorKey: "_source.sourcedMetadata.comicInfo",
|
cell: (info) =>
|
||||||
align: "center",
|
!isEmpty(info.getValue()) ? (
|
||||||
minWidth: 250,
|
<ComicInfoXML data={info.getValue()} />
|
||||||
cell: info =>
|
) : null,
|
||||||
!isEmpty(info.getValue()) ? (
|
|
||||||
<ComicInfoXML data={info.getValue()} />
|
|
||||||
) : (
|
|
||||||
<span className="tag mt-5">No ComicInfo.xml</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Additional Metadata",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "Publisher",
|
|
||||||
accessorKey:
|
|
||||||
"_source.sourcedMetadata.comicvine.volumeInformation",
|
|
||||||
cell: info => {
|
|
||||||
return (
|
|
||||||
!isNil(info.getValue()) && (
|
|
||||||
<h6 className="is-size-7 has-text-weight-bold">
|
|
||||||
{info.getValue().publisher.name}
|
|
||||||
</h6>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
header: "Something",
|
{
|
||||||
accessorKey: "_source.acquisition.source.wanted",
|
header: "Additional Metadata",
|
||||||
cell: info => {
|
columns: [
|
||||||
!isUndefined(info.getValue()) ?
|
{
|
||||||
<WantedStatus value={info.getValue().toString()} /> : "Nothing";
|
header: "Date of Import",
|
||||||
|
accessorKey: "_source.createdAt",
|
||||||
|
cell: (info) => {
|
||||||
|
return !isNil(info.getValue()) ? (
|
||||||
|
<div className="text-xs w-max ml-3 my-3 text-slate-600">
|
||||||
|
<p>{format(parseISO(info.getValue()), "dd MMMM, yyyy")} </p>
|
||||||
|
{format(parseISO(info.getValue()), "h aaaa")}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
header: "Downloads",
|
||||||
}
|
accessorKey: "_source.acquisition",
|
||||||
], []);
|
cell: (info) => (
|
||||||
|
<div className="flex flex-col gap-2 ml-3 my-3">
|
||||||
|
<span className="inline-flex items-center w-fit bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--folder-path-connect-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
DC++: {info.getValue().directconnect.downloads.length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="inline-flex w-fit items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
|
Torrent: {info.getValue().torrent.downloads.length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination control that fetches the next x (pageSize) items
|
* Pagination control that fetches the next x (pageSize) items
|
||||||
@@ -153,26 +167,13 @@ export const Library = (): ReactElement => {
|
|||||||
* @param {number} pageIndex
|
* @param {number} pageIndex
|
||||||
* @param {number} pageSize
|
* @param {number} pageSize
|
||||||
* @returns void
|
* @returns void
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
|
const nextPage = (pageIndex: number, pageSize: number) => {
|
||||||
dispatch(
|
if (!isPlaceholderData) {
|
||||||
searchIssue(
|
setOffset(pageSize * pageIndex + 1);
|
||||||
{
|
}
|
||||||
query: {},
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
pagination: {
|
|
||||||
size: pageSize,
|
|
||||||
from: pageSize * pageIndex + 1,
|
|
||||||
},
|
|
||||||
type: "all",
|
|
||||||
trigger: "libraryPage",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination control that fetches the previous x (pageSize) items
|
* Pagination control that fetches the previous x (pageSize) items
|
||||||
@@ -181,46 +182,44 @@ export const Library = (): ReactElement => {
|
|||||||
* @param {number} pageSize
|
* @param {number} pageSize
|
||||||
* @returns void
|
* @returns void
|
||||||
**/
|
**/
|
||||||
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
|
const previousPage = (pageIndex: number, pageSize: number) => {
|
||||||
let from = 0;
|
let from = 0;
|
||||||
if (pageIndex === 2) {
|
if (pageIndex === 2) {
|
||||||
from = (pageIndex - 1) * pageSize + 2 - 17;
|
from = (pageIndex - 1) * pageSize + 2 - (pageSize + 2);
|
||||||
} else {
|
} else {
|
||||||
from = (pageIndex - 1) * pageSize + 2 - 16;
|
from = (pageIndex - 1) * pageSize + 2 - (pageSize + 1);
|
||||||
}
|
}
|
||||||
dispatch(
|
setOffset(from);
|
||||||
searchIssue(
|
};
|
||||||
{
|
|
||||||
query: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pagination: {
|
|
||||||
size: pageSize,
|
|
||||||
from,
|
|
||||||
},
|
|
||||||
type: "all",
|
|
||||||
trigger: "libraryPage"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ImportStatus.propTypes = {
|
// ImportStatus.propTypes = {
|
||||||
// value: PropTypes.bool.isRequired,
|
// value: PropTypes.bool.isRequired,
|
||||||
// };
|
// };
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<div>
|
||||||
<div className="section">
|
<section>
|
||||||
<div className="header-area">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<h1 className="title">Library</h1>
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
</div>
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
{!isEmpty(searchResults) ? (
|
<div className="text-center sm:text-left">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
|
Library
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Browse your comic book collection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{!isUndefined(searchResults?.hits) ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="library">
|
<div className="library">
|
||||||
<T2Table
|
<T2Table
|
||||||
totalPages={searchResults.hits.total.value}
|
totalPages={searchResults.hits.total.value}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sourceData={searchResults?.hits?.hits}
|
sourceData={searchResults?.hits.hits}
|
||||||
rowClickHandler={navigateToComicDetail}
|
rowClickHandler={navigateToComicDetail}
|
||||||
paginationHandlers={{
|
paginationHandlers={{
|
||||||
nextPage,
|
nextPage,
|
||||||
@@ -229,27 +228,37 @@ export const Library = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
): <div className="columns">
|
) : (
|
||||||
<div className="column is-two-thirds">
|
<>
|
||||||
<article className="message is-link">
|
<article
|
||||||
<div className="message-body">
|
role="alert"
|
||||||
No comics were found in the library, Elasticsearch reports no
|
className="rounded-lg max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600"
|
||||||
indices. Try importing a few comics into the library and come
|
>
|
||||||
back.
|
<div>
|
||||||
|
<p>
|
||||||
|
No comics were found in the library, Elasticsearch reports no
|
||||||
|
indices. Try importing a few comics into the library and come
|
||||||
|
back.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<pre>
|
<div className="block max-w-md p-6 bg-white border border-gray-200 my-3 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
{!isUndefined(searchError.data) &&
|
<pre className="text-sm font-hasklig text-slate-700 dark:text-slate-700">
|
||||||
JSON.stringify(
|
{!isUndefined(searchResults?.data?.meta?.body) ? (
|
||||||
searchError.data.meta.body.error.root_cause,
|
<p>
|
||||||
null,
|
{JSON.stringify(
|
||||||
4,
|
searchResults.data.meta.body.error.root_cause,
|
||||||
)}
|
null,
|
||||||
</pre>
|
4,
|
||||||
</div>
|
)}
|
||||||
</div> }
|
</p>
|
||||||
</div>
|
) : null}
|
||||||
</section>
|
</pre>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -73,7 +73,7 @@ export const LibraryGrid = (libraryGridProps: ILibraryGridProps) => {
|
|||||||
<div className="content is-flex is-flex-direction-row">
|
<div className="content is-flex is-flex-direction-row">
|
||||||
{!isEmpty(sourcedMetadata.comicvine) && (
|
{!isEmpty(sourcedMetadata.comicvine) && (
|
||||||
<span className="icon cv-icon is-small">
|
<span className="icon cv-icon is-small">
|
||||||
<img src="/dist/img/cvlogo.svg" />
|
<img src="/src/client/assets/img/cvlogo.svg" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isNil(rawFileDetails) && (
|
{isNil(rawFileDetails) && (
|
||||||
|
|||||||
@@ -2,62 +2,65 @@ import React, { ReactElement, useCallback } from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Form, Field } from "react-final-form";
|
import { Form, Field } from "react-final-form";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { searchIssue } from "../../actions/fileops.actions";
|
import { searchIssue } from "../../actions/fileops.actions";
|
||||||
|
|
||||||
export const SearchBar = (): ReactElement => {
|
export const SearchBar = (): ReactElement => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const handleSubmit = useCallback((e) => {
|
const handleSubmit = useCallback((e) => {
|
||||||
dispatch(
|
// dispatch(
|
||||||
searchIssue(
|
// searchIssue(
|
||||||
{
|
// {
|
||||||
query: {
|
// query: {
|
||||||
volumeName: e.search,
|
// volumeName: e.search,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
pagination: {
|
// pagination: {
|
||||||
size: 25,
|
// size: 25,
|
||||||
from: 0,
|
// from: 0,
|
||||||
},
|
// },
|
||||||
type: "volumeName",
|
// type: "volumeName",
|
||||||
trigger: "libraryPage",
|
// trigger: "libraryPage",
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="box">
|
<Form
|
||||||
<Form
|
onSubmit={handleSubmit}
|
||||||
onSubmit={handleSubmit}
|
initialValues={{}}
|
||||||
initialValues={{}}
|
render={({ handleSubmit, form, submitting, pristine, values }) => (
|
||||||
render={({ handleSubmit, form, submitting, pristine, values }) => (
|
<form onSubmit={handleSubmit}>
|
||||||
<form onSubmit={handleSubmit}>
|
<Field name="search">
|
||||||
<div className="field is-grouped">
|
{({ input, meta }) => {
|
||||||
<div className="control search is-expanded">
|
return (
|
||||||
<Field name="search">
|
<div className="flex flex-row w-full">
|
||||||
{({ input, meta }) => {
|
<div className="flex flex-row bg-slate-300 dark:bg-slate-500 rounded-l-lg p-2 min-w-full">
|
||||||
return (
|
<div className="w-10 text-gray-400">
|
||||||
<input
|
<i className="icon-[solar--magnifer-bold-duotone] h-7 w-7" />
|
||||||
{...input}
|
</div>
|
||||||
className="input main-search-bar is-medium"
|
|
||||||
placeholder="Type an issue/volume name"
|
<input
|
||||||
/>
|
{...input}
|
||||||
);
|
className="bg-slate-300 dark:bg-slate-500 outline-none text-lg text-gray-700 w-full"
|
||||||
}}
|
type="text"
|
||||||
</Field>
|
id="search"
|
||||||
</div>
|
placeholder="Type an issue/volume name"
|
||||||
<div className="control">
|
/>
|
||||||
<button className="button is-medium" type="submit">
|
</div>
|
||||||
Search
|
|
||||||
</button>
|
<button
|
||||||
</div>
|
className="sm:mt-0 rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
</div>
|
type="submit"
|
||||||
</form>
|
>
|
||||||
)}
|
Search
|
||||||
/>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
);
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import React, { ReactElement, useEffect, useMemo } from "react";
|
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 Card from "../shared/Carda";
|
||||||
import Card from "../Carda";
|
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { isNil } from "lodash";
|
import { isNil } from "lodash";
|
||||||
|
|
||||||
export const PullList = (): ReactElement => {
|
export const PullList = (): ReactElement => {
|
||||||
const pullListComics = useSelector(
|
// const pullListComics = useSelector(
|
||||||
(state: RootState) => state.comicInfo.pullList,
|
// (state: RootState) => state.comicInfo.pullList,
|
||||||
);
|
// );
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
// dispatch(
|
||||||
getWeeklyPullList({
|
// getWeeklyPullList({
|
||||||
startDate: "2022-11-15",
|
// startDate: "2023-7-28",
|
||||||
pageSize: "15",
|
// pageSize: "15",
|
||||||
currentPage: "1",
|
// currentPage: "1",
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
const nextPageHandler = () => {};
|
const nextPageHandler = () => {};
|
||||||
const previousPageHandler = () => {};
|
const previousPageHandler = () => {};
|
||||||
@@ -109,15 +107,15 @@ export const PullList = (): ReactElement => {
|
|||||||
{!isNil(pullListComics) && (
|
{!isNil(pullListComics) && (
|
||||||
<div>
|
<div>
|
||||||
<div className="library">
|
<div className="library">
|
||||||
<T2Table
|
<T2Table
|
||||||
sourceData={pullListComics}
|
sourceData={pullListComics}
|
||||||
columns={columnData}
|
columns={columnData}
|
||||||
totalPages={pullListComics.length}
|
totalPages={pullListComics.length}
|
||||||
paginationHandlers={{
|
paginationHandlers={{
|
||||||
nextPage: nextPageHandler,
|
nextPage: nextPageHandler,
|
||||||
previousPage: previousPageHandler,
|
previousPage: previousPageHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useMemo, 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";
|
||||||
25
src/client/components/ServiceStatuses/ServiceStatuses.tsx
Normal file
25
src/client/components/ServiceStatuses/ServiceStatuses.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { getServiceStatus } from "../../actions/fileops.actions";
|
||||||
|
|
||||||
|
export const ServiceStatuses = (): ReactElement => {
|
||||||
|
const serviceStatus = useSelector(
|
||||||
|
(state: RootState) => state.fileOps.libraryServiceStatus,
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getServiceStatus());
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div className="is-clearfix">
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="title">Core Services</h3>
|
||||||
|
<h6 className="subtitle has-text-grey-light">
|
||||||
|
Statuses for core services
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(serviceStatus, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState, useContext } from "react";
|
||||||
|
import { Form, Field } from "react-final-form";
|
||||||
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
|
import Select from "react-select";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useStore } from "../../../store";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const AirDCPPHubsForm = (): ReactElement => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const {
|
||||||
|
airDCPPSocketInstance,
|
||||||
|
airDCPPClientConfiguration,
|
||||||
|
airDCPPSessionInformation,
|
||||||
|
} = useStore((state) => ({
|
||||||
|
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
||||||
|
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
|
||||||
|
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: settings,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["settings"],
|
||||||
|
queryFn: async () =>
|
||||||
|
await axios({
|
||||||
|
url: "http://localhost:3000/api/settings/getAllSettings",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hubs list from an AirDCPP Socket
|
||||||
|
*/
|
||||||
|
const { data: hubs } = useQuery({
|
||||||
|
queryKey: ["hubs"],
|
||||||
|
queryFn: async () => await airDCPPSocketInstance.get(`hubs`),
|
||||||
|
});
|
||||||
|
let hubList = {};
|
||||||
|
if (!isNil(hubs)) {
|
||||||
|
hubList = hubs.map(({ hub_url, identity }) => ({
|
||||||
|
value: hub_url,
|
||||||
|
label: identity.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: async (values) =>
|
||||||
|
await axios({
|
||||||
|
url: `http://localhost:3000/api/settings/saveSettings`,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
settingsPayload: values,
|
||||||
|
settingsObjectId: settings?.data._id,
|
||||||
|
settingsKey: "directConnect",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const validate = async () => {};
|
||||||
|
|
||||||
|
const SelectAdapter = ({ input, ...rest }) => {
|
||||||
|
return <Select {...input} {...rest} isClearable isMulti />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isEmpty(hubList) && !isUndefined(hubs) ? (
|
||||||
|
<Form
|
||||||
|
onSubmit={mutate}
|
||||||
|
validate={validate}
|
||||||
|
render={({ handleSubmit }) => (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<h3 className="title">Hubs</h3>
|
||||||
|
<h6 className="subtitle has-text-grey-light">
|
||||||
|
Select the hubs you want to perform searches against.
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">AirDC++ Host</label>
|
||||||
|
<div className="control">
|
||||||
|
<Field
|
||||||
|
name="hubs"
|
||||||
|
component={SelectAdapter}
|
||||||
|
className="basic-multi-select"
|
||||||
|
placeholder="Select Hubs to Search Against"
|
||||||
|
options={hubList}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="button is-primary">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<article className="message">
|
||||||
|
<div className="message-body">
|
||||||
|
No configured hubs detected in AirDC++. <br />
|
||||||
|
Configure to a hub in AirDC++ and then select a default hub here.
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isEmpty(settings?.data.directConnect?.client.hubs) ? (
|
||||||
|
<>
|
||||||
|
<div className="mt-4">
|
||||||
|
<article className="message is-warning">
|
||||||
|
<div className="message-body is-size-6 is-family-secondary">
|
||||||
|
Your selection in the dropdown <strong>will replace</strong> the
|
||||||
|
existing selection.
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div className="box mt-3">
|
||||||
|
<h6>Default Hub For Searches:</h6>
|
||||||
|
{settings?.data.directConnect?.client.hubs.map(
|
||||||
|
({ value, label }) => (
|
||||||
|
<div key={value}>
|
||||||
|
<div>{label}</div>
|
||||||
|
<span className="is-size-7">{value}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AirDCPPHubsForm;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
|
||||||
|
export const AirDCPPSettingsConfirmation = (settingsObject): ReactElement => {
|
||||||
|
const { settings } = settingsObject;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="flex items-center mt-10 mb-4">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
AirDC++ Client Information
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
<div className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
|
<span className="inline-flex justify-center rounded-full bg-emerald-100 mb-4 px-2 py-0.5 text-emerald-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--plug-circle-bold] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-nowrap text-sm">Connected</p>
|
||||||
|
</span>
|
||||||
|
<p className="font-hasklig text-sm text-slate-700 dark:text-slate-700">
|
||||||
|
<dl>
|
||||||
|
<dt>{settings._id}</dt>
|
||||||
|
<dt>Client version: {settings.system_info.client_version}</dt>
|
||||||
|
<dt>Hostname: {settings.system_info.hostname}</dt>
|
||||||
|
<dt>Platform: {settings.system_info.platform}</dt>
|
||||||
|
|
||||||
|
<dt>Username: {settings.user.username}</dt>
|
||||||
|
|
||||||
|
<dt>Active Sessions: {settings.user.active_sessions}</dt>
|
||||||
|
<dt>
|
||||||
|
Permissions:{" "}
|
||||||
|
{JSON.stringify(settings.user.permissions, undefined, 2)}
|
||||||
|
</dt>
|
||||||
|
</dl>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AirDCPPSettingsConfirmation;
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import React, { ReactElement, useCallback } from "react";
|
||||||
|
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
|
||||||
|
import { isUndefined, isEmpty } from "lodash";
|
||||||
|
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
|
||||||
|
import { initializeAirDCPPSocket, useStore } from "../../../store/index";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const AirDCPPSettingsForm = (): ReactElement => {
|
||||||
|
// cherry-picking selectors for:
|
||||||
|
// 1. initial values for the form
|
||||||
|
// 2. If initial values are present, get the socket information to display
|
||||||
|
const { setState } = useStore;
|
||||||
|
const {
|
||||||
|
airDCPPSocketConnected,
|
||||||
|
airDCPPDisconnectionInfo,
|
||||||
|
airDCPPSessionInformation,
|
||||||
|
airDCPPClientConfiguration,
|
||||||
|
airDCPPSocketInstance,
|
||||||
|
setAirDCPPSocketInstance,
|
||||||
|
} = useStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
airDCPPSocketConnected: state.airDCPPSocketConnected,
|
||||||
|
airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo,
|
||||||
|
airDCPPClientConfiguration: state.airDCPPClientConfiguration,
|
||||||
|
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
||||||
|
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
||||||
|
setAirDCPPSocketInstance: state.setAirDCPPSocketInstance,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutation to update settings and subsequently initialize
|
||||||
|
* AirDC++ socket with those settings
|
||||||
|
*/
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: async (values) =>
|
||||||
|
await axios({
|
||||||
|
url: `http://localhost:3000/api/settings/saveSettings`,
|
||||||
|
method: "POST",
|
||||||
|
data: { settingsPayload: values, settingsKey: "directConnect" },
|
||||||
|
}),
|
||||||
|
onSuccess: async (values) => {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
directConnect: {
|
||||||
|
client: { host },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = values;
|
||||||
|
const dcppSocketInstance = await initializeAirDCPPSocket(host);
|
||||||
|
setState({
|
||||||
|
airDCPPClientConfiguration: host,
|
||||||
|
airDCPPSocketInstance: dcppSocketInstance,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const deleteSettingsMutation = useMutation(
|
||||||
|
async () =>
|
||||||
|
await axios.post("http://localhost:3000/api/settings/saveSettings", {
|
||||||
|
settingsPayload: {},
|
||||||
|
settingsKey: "directConnect",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// const removeSettings = useCallback(async () => {
|
||||||
|
// // airDCPPSettings.setSettings({});
|
||||||
|
// }, []);
|
||||||
|
//
|
||||||
|
const initFormData = !isUndefined(airDCPPClientConfiguration)
|
||||||
|
? airDCPPClientConfiguration
|
||||||
|
: {};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConnectionForm
|
||||||
|
initialData={initFormData}
|
||||||
|
submitHandler={mutate}
|
||||||
|
formHeading={"Configure AirDC++"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isEmpty(airDCPPSessionInformation) ? (
|
||||||
|
<AirDCPPSettingsConfirmation settings={airDCPPSessionInformation} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isEmpty(airDCPPClientConfiguration) ? (
|
||||||
|
<p className="control mt-4">
|
||||||
|
as
|
||||||
|
<button
|
||||||
|
className="button is-danger"
|
||||||
|
onClick={() => deleteSettingsMutation.mutate()}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AirDCPPSettingsForm;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
|
||||||
|
import { useQuery, useMutation, QueryClient } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const QbittorrentConnectionForm = (): ReactElement => {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
// fetch settings
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["settings"],
|
||||||
|
queryFn: async () =>
|
||||||
|
await axios({
|
||||||
|
url: "http://localhost:3000/api/settings/getAllSettings",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
// 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" },
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["settings", "qbittorrentClientInfo"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isError)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<pre>Something went wrong connecting to qBittorrent.</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
if (!isLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConnectionForm
|
||||||
|
initialData={hostDetails}
|
||||||
|
formHeading={"qBittorrent Configuration"}
|
||||||
|
submitHandler={mutate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="flex items-center mt-10 mb-4">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
qBittorrent Client Information
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
|
<span className="inline-flex justify-center rounded-full bg-emerald-100 mb-4 px-2 py-0.5 text-emerald-700">
|
||||||
|
<span className="h-5 w-6">
|
||||||
|
<i className="icon-[solar--plug-circle-bold] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-nowrap text-sm">Connected</p>
|
||||||
|
</span>
|
||||||
|
<pre className="font-hasklig text-sm text-slate-700 dark:text-slate-700">
|
||||||
|
{JSON.stringify(qbittorrentClientInfo?.data, null, 4)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QbittorrentConnectionForm;
|
||||||
@@ -1,8 +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 settingsObject from "../constants/settings/settingsMenu.json";
|
import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
|
||||||
|
import settingsObject from "../../constants/settings/settingsMenu.json";
|
||||||
import { isUndefined, map } from "lodash";
|
import { isUndefined, map } from "lodash";
|
||||||
|
|
||||||
interface ISettingsProps {}
|
interface ISettingsProps {}
|
||||||
@@ -12,7 +14,11 @@ 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",
|
||||||
@@ -22,6 +28,18 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "qbt-connection",
|
||||||
|
content: (
|
||||||
|
<div key="qbt-connection">
|
||||||
|
<QbittorrentConnectionForm />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "core-service",
|
||||||
|
content: <>a</>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "flushdb",
|
id: "flushdb",
|
||||||
content: (
|
content: (
|
||||||
@@ -32,24 +50,45 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<div>
|
||||||
<div className="columns">
|
<section>
|
||||||
<div className="section column is-one-quarter">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<h1 className="title">Settings</h1>
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
<aside className="menu">
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Import comics into the ThreeTwo library.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-cols max-w-screen-xl mx-auto">
|
||||||
|
<aside className="px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||||
{map(settingsObject, (settingObject, idx) => {
|
{map(settingsObject, (settingObject, idx) => {
|
||||||
return (
|
return (
|
||||||
<div key={idx}>
|
<div
|
||||||
<p className="menu-label">{settingObject.category}</p>
|
className="w-64 py-2 text-slate-700 dark:text-slate-400"
|
||||||
|
key={idx}
|
||||||
|
>
|
||||||
|
<h3 className="text-l pb-2">
|
||||||
|
{settingObject.category.toUpperCase()}
|
||||||
|
</h3>
|
||||||
{/* First level children */}
|
{/* First level children */}
|
||||||
{!isUndefined(settingObject.children) ? (
|
{!isUndefined(settingObject.children) ? (
|
||||||
<ul className="menu-list" key={settingObject.id}>
|
<ul key={settingObject.id}>
|
||||||
{map(settingObject.children, (item, idx) => {
|
{map(settingObject.children, (item, idx) => {
|
||||||
return (
|
return (
|
||||||
<li key={idx}>
|
<li key={idx} className="mb-2">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
item.id.toString() === active ? "is-active" : ""
|
item.id.toString() === active
|
||||||
|
? "is-active flex items-center"
|
||||||
|
: "flex items-center"
|
||||||
}
|
}
|
||||||
onClick={() => setActive(item.id.toString())}
|
onClick={() => setActive(item.id.toString())}
|
||||||
>
|
>
|
||||||
@@ -57,14 +96,14 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
</a>
|
</a>
|
||||||
{/* Second level children */}
|
{/* Second level children */}
|
||||||
{!isUndefined(item.children) ? (
|
{!isUndefined(item.children) ? (
|
||||||
<ul>
|
<ul className="pl-4 mt-2">
|
||||||
{map(item.children, (item, idx) => (
|
{map(item.children, (item, idx) => (
|
||||||
<li key={item.id}>
|
<li key={item.id} className="mb-2">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
item.id.toString() === active
|
item.id.toString() === active
|
||||||
? "is-active"
|
? "is-active flex items-center"
|
||||||
: ""
|
: "flex items-center"
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setActive(item.id.toString())
|
setActive(item.id.toString())
|
||||||
@@ -85,18 +124,18 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* content for settings */}
|
{/* content for settings */}
|
||||||
<div className="section column is-half mt-6">
|
<div className="max-w-screen-xl">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{map(settingsContent, ({ id, content }) =>
|
{map(settingsContent, ({ id, content }) =>
|
||||||
active === id ? content : null,
|
active === id ? content : null,
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import React, { ReactElement, useCallback } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { flushDb } from "../../actions/settings.actions";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import axios from "axios";
|
||||||
|
|
||||||
export const SystemSettingsForm = (): ReactElement => {
|
export const SystemSettingsForm = (): ReactElement => {
|
||||||
const dispatch = useDispatch();
|
const { mutate: flushDb, isLoading } = useMutation({
|
||||||
const isSettingsCallInProgress = useSelector(
|
mutationFn: async () => {
|
||||||
(state: RootState) => state.settings.inProgress,
|
await axios({
|
||||||
);
|
url: `http://localhost:3000/api/library/flushDb`,
|
||||||
const flushDatabase = useCallback(() => {
|
method: "POST",
|
||||||
dispatch(flushDb());
|
});
|
||||||
}, []);
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="is-clearfix">
|
<div className="is-clearfix">
|
||||||
@@ -48,11 +49,9 @@ export const SystemSettingsForm = (): ReactElement => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
isSettingsCallInProgress
|
isLoading ? "button is-danger is-loading" : "button is-danger"
|
||||||
? "button is-danger is-loading"
|
|
||||||
: "button is-danger"
|
|
||||||
}
|
}
|
||||||
onClick={flushDatabase}
|
onClick={() => flushDb()}
|
||||||
>
|
>
|
||||||
<span className="icon">
|
<span className="icon">
|
||||||
<i className="fas fa-eraser"></i>
|
<i className="fas fa-eraser"></i>
|
||||||
@@ -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,31 +1,29 @@
|
|||||||
import React, { ReactElement, useEffect, useMemo } from "react";
|
import React, { ReactElement, useEffect, useMemo } from "react";
|
||||||
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";
|
||||||
import { isUndefined } from "lodash";
|
import { isUndefined } from "lodash";
|
||||||
|
|
||||||
export const Volumes = (props): ReactElement => {
|
export const Volumes = (props): ReactElement => {
|
||||||
const volumes = useSelector((state: RootState) => state.fileOps.volumes);
|
// const volumes = useSelector((state: RootState) => state.fileOps.volumes);
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
// dispatch(
|
||||||
searchIssue(
|
// searchIssue(
|
||||||
{
|
// {
|
||||||
query: {},
|
// query: {},
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
pagination: {
|
// pagination: {
|
||||||
size: 25,
|
// size: 25,
|
||||||
from: 0,
|
// from: 0,
|
||||||
},
|
// },
|
||||||
type: "volumes",
|
// type: "volumes",
|
||||||
trigger: "volumesPage",
|
// trigger: "volumesPage",
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
const columnData = useMemo(
|
const columnData = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -166,8 +164,8 @@ export const Volumes = (props): ReactElement => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="library">
|
<div className="library">
|
||||||
<T2Table
|
<T2Table
|
||||||
sourceData={volumes?.hits?.hits}
|
sourceData={volumes?.hits}
|
||||||
totalPages={volumes.hits.hits.length}
|
totalPages={volumes.hits.length}
|
||||||
paginationHandlers={{
|
paginationHandlers={{
|
||||||
nextPage: () => {},
|
nextPage: () => {},
|
||||||
previousPage: () => {},
|
previousPage: () => {},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useMemo } from "react";
|
import React, { ReactElement, useCallback, useEffect, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { searchIssue } from "../../actions/fileops.actions";
|
import { searchIssue } from "../../actions/fileops.actions";
|
||||||
import SearchBar from "../Library/SearchBar";
|
import SearchBar from "../Library/SearchBar";
|
||||||
import T2Table from "../shared/T2Table";
|
import T2Table from "../shared/T2Table";
|
||||||
@@ -7,26 +6,25 @@ import { isEmpty, isUndefined } from "lodash";
|
|||||||
import MetadataPanel from "../shared/MetadataPanel";
|
import MetadataPanel from "../shared/MetadataPanel";
|
||||||
|
|
||||||
export const WantedComics = (props): ReactElement => {
|
export const WantedComics = (props): ReactElement => {
|
||||||
const wantedComics = useSelector(
|
// const wantedComics = useSelector(
|
||||||
(state: RootState) => state.fileOps.wantedComics,
|
// (state: RootState) => state.fileOps.wantedComics,
|
||||||
);
|
// );
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
// dispatch(
|
||||||
searchIssue(
|
// searchIssue(
|
||||||
{
|
// {
|
||||||
query: {},
|
// query: {},
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
pagination: {
|
// pagination: {
|
||||||
size: 25,
|
// size: 25,
|
||||||
from: 0,
|
// from: 0,
|
||||||
},
|
// },
|
||||||
type: "wanted",
|
// type: "wanted",
|
||||||
trigger: "wantedComicsPage"
|
// trigger: "wantedComicsPage"
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columnData = [
|
const columnData = [
|
||||||
@@ -37,7 +35,7 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
header: "Details",
|
header: "Details",
|
||||||
id: "comicDetails",
|
id: "comicDetails",
|
||||||
minWidth: 350,
|
minWidth: 350,
|
||||||
accessorFn: data => data,
|
accessorFn: (data) => data,
|
||||||
cell: (value) => <MetadataPanel data={value.getValue()} />,
|
cell: (value) => <MetadataPanel data={value.getValue()} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -49,8 +47,10 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
header: "Files",
|
header: "Files",
|
||||||
accessorKey: "acquisition",
|
accessorKey: "acquisition",
|
||||||
align: "right",
|
align: "right",
|
||||||
cell: props => {
|
cell: (props) => {
|
||||||
const { directconnect: { downloads } } = props.getValue();
|
const {
|
||||||
|
directconnect: { downloads },
|
||||||
|
} = props.getValue();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -60,9 +60,7 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{downloads.length > 0 ? (
|
{downloads.length > 0 ? (
|
||||||
<span className="tag is-warning">
|
<span className="tag is-warning">{downloads.length}</span>
|
||||||
{downloads.length}
|
|
||||||
</span>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -72,11 +70,17 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
header: "Download Details",
|
header: "Download Details",
|
||||||
id: "downloadDetails",
|
id: "downloadDetails",
|
||||||
accessorKey: "acquisition",
|
accessorKey: "acquisition",
|
||||||
cell: data => <ol>
|
cell: (data) => (
|
||||||
{data.getValue().directconnect.downloads.map(download => {
|
<ol>
|
||||||
return <li className="is-size-7">{download.name}</li>;
|
{data.getValue().directconnect.downloads.map((download, idx) => {
|
||||||
})}
|
return (
|
||||||
</ol>
|
<li className="is-size-7" key={idx}>
|
||||||
|
{download.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Type",
|
header: "Type",
|
||||||
@@ -92,7 +96,7 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
* @param {number} pageIndex
|
* @param {number} pageIndex
|
||||||
* @param {number} pageSize
|
* @param {number} pageSize
|
||||||
* @returns void
|
* @returns void
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
|
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -112,7 +116,6 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination control that fetches the previous x (pageSize) items
|
* Pagination control that fetches the previous x (pageSize) items
|
||||||
* based on the y (pageIndex) offset from the Elasticsearch index
|
* based on the y (pageIndex) offset from the Elasticsearch index
|
||||||
@@ -138,7 +141,7 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
from,
|
from,
|
||||||
},
|
},
|
||||||
type: "wanted",
|
type: "wanted",
|
||||||
trigger: "wantedComicsPage"
|
trigger: "wantedComicsPage",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -161,7 +164,7 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
nextPage: nextPage,
|
nextPage: nextPage,
|
||||||
previousPage: previousPage,
|
previousPage: previousPage,
|
||||||
}}
|
}}
|
||||||
// rowClickHandler={navigateToComicDetail}
|
// rowClickHandler={navigateToComicDetail}
|
||||||
/>
|
/>
|
||||||
{/* pagination controls */}
|
{/* pagination controls */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
171
src/client/components/shared/Carda.tsx
Normal file
171
src/client/components/shared/Carda.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { isEmpty, isNil } from "lodash";
|
||||||
|
|
||||||
|
interface ICardProps {
|
||||||
|
orientation: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
hasDetails?: boolean;
|
||||||
|
title?: PropTypes.ReactElementLike | null;
|
||||||
|
children?: PropTypes.ReactNodeLike;
|
||||||
|
borderColorClass?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
cardContainerStyle?: PropTypes.object;
|
||||||
|
imageStyle?: PropTypes.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCard = (props: ICardProps): ReactElement => {
|
||||||
|
switch (props.orientation) {
|
||||||
|
case "horizontal":
|
||||||
|
return (
|
||||||
|
<div className="card-container">
|
||||||
|
<div className="card generic-card">
|
||||||
|
<div className="is-horizontal">
|
||||||
|
<div className="card-image">
|
||||||
|
<img
|
||||||
|
style={props.imageStyle}
|
||||||
|
src={props.imageUrl}
|
||||||
|
alt="Placeholder image"
|
||||||
|
className="cropped-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{props.hasDetails && (
|
||||||
|
<div className="card-content">{props.children}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "vertical":
|
||||||
|
return (
|
||||||
|
<div onClick={props.onClick}>
|
||||||
|
<div className="generic-card" style={props.cardContainerStyle}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
!isNil(props.borderColorClass)
|
||||||
|
? `${props.borderColorClass}`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
props.hasDetails
|
||||||
|
? "partial-rounded-card-image"
|
||||||
|
: "rounded-card-image"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src={props.imageUrl}
|
||||||
|
style={props.imageStyle}
|
||||||
|
alt="Placeholder image"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
{props.hasDetails && (
|
||||||
|
<div
|
||||||
|
className="card-content"
|
||||||
|
style={{ backgroundColor: props.backgroundColor }}
|
||||||
|
>
|
||||||
|
{!isNil(props.title) ? (
|
||||||
|
<div className="card-title is-size-8 is-family-secondary">
|
||||||
|
{props.title}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "vertical-2":
|
||||||
|
return (
|
||||||
|
<div className="block rounded-md w-fit h-fit shadow-md shadow-white-400 bg-gray-200 dark:bg-slate-500">
|
||||||
|
<img
|
||||||
|
alt="Home"
|
||||||
|
src={props.imageUrl}
|
||||||
|
className="rounded-t-md object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2 px-2">
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dd className="text-md text-slate-500 dark:text-black">
|
||||||
|
{props.title}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{props.hasDetails && <>{props.children}</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "horizontal-small":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row justify-start align-top gap-3 bg-slate-200 h-fit rounded-md shadow-md shadow-white-400">
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden">
|
||||||
|
<img src={props.imageUrl} className="object-cover h-20 w-20" />
|
||||||
|
</div>
|
||||||
|
{/* details */}
|
||||||
|
<div className="w-fit h-fit pl-1 pr-2 py-1">
|
||||||
|
<p className="text-sm">{props.title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "horizontal-medium":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row items-center align-top gap-3 bg-slate-200 h-fit p-2 rounded-md shadow-md shadow-white-400">
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
{/* details */}
|
||||||
|
<div className="pl-1 pr-2 py-1">
|
||||||
|
<p className="text-sm">{props.title}</p>
|
||||||
|
{props.hasDetails && <>{props.children}</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "cover-only":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-lg shadow-lg overflow-hidden w-fit h-fit">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "card-with-info-panel":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden w-fit h-fit">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
{/* myata-dyata */}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Card = React.memo(
|
||||||
|
(props: ICardProps): ReactElement => renderCard(props),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Card;
|
||||||
158
src/client/components/shared/ConnectionForm/ConnectionForm.tsx
Normal file
158
src/client/components/shared/ConnectionForm/ConnectionForm.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
import { Form, Field } from "react-final-form";
|
||||||
|
import { hostNameValidator } from "../../../shared/utils/validator.utils";
|
||||||
|
import { isEmpty } from "lodash";
|
||||||
|
|
||||||
|
export const ConnectionForm = ({
|
||||||
|
initialData,
|
||||||
|
submitHandler,
|
||||||
|
formHeading,
|
||||||
|
}): ReactElement => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
onSubmit={submitHandler}
|
||||||
|
initialValues={initialData}
|
||||||
|
render={({ handleSubmit }) => (
|
||||||
|
<form onSubmit={handleSubmit} className="mt-10">
|
||||||
|
<h2 className="text-xl">{formHeading}</h2>
|
||||||
|
<article
|
||||||
|
role="alert"
|
||||||
|
className="mt-4 rounded-lg max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>Configure your AirDC++ client connection here.</p>
|
||||||
|
<p>
|
||||||
|
Note that you need an instance of AirDC++ already running to
|
||||||
|
use this form to connect to it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
See{" "}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="http://airdcpp.net/docs/installation/installation.html"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>{" "}
|
||||||
|
for AirDC++ installation instructions for various platforms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<span className="flex items-center mt-6">
|
||||||
|
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
|
||||||
|
Configure Connection
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex flex-row mt-4">
|
||||||
|
<div className="relative">
|
||||||
|
{/* protocol */}
|
||||||
|
<label className="block py-1">Protocol</label>
|
||||||
|
<Field
|
||||||
|
name="protocol"
|
||||||
|
component="select"
|
||||||
|
className="appearance-none dark:bg-slate-400 bg-slate-100 h-10 rounded-md border-none text-gray-700 dark:text-slate-200 py-1 pr-7 pl-3 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
>
|
||||||
|
<option>Protocol</option>
|
||||||
|
<option value="http">http://</option>
|
||||||
|
<option value="https">https://</option>
|
||||||
|
</Field>
|
||||||
|
<div className="absolute h-7 w-7 right-0 px-1 top-11 pointer-events-none">
|
||||||
|
<i className="icon-[solar--alt-arrow-down-bold]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* hostname */}
|
||||||
|
<Field name="hostname" validate={hostNameValidator}>
|
||||||
|
{({ input, meta }) => (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="block px-2 py-1">Hostname</label>
|
||||||
|
<input
|
||||||
|
{...input}
|
||||||
|
type="text"
|
||||||
|
placeholder="Hostname"
|
||||||
|
className="ml-2 dark:bg-slate-400 bg-slate-100 py-2 px-2 rounded-md border-gray-300 h-10 dark:text-slate-200 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{meta.error && meta.touched && (
|
||||||
|
<span className="text-sm text-red-400 px-2">
|
||||||
|
{meta.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{/* port */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="block px-2 py-1">Port</label>
|
||||||
|
<Field
|
||||||
|
name="port"
|
||||||
|
component="input"
|
||||||
|
className="ml-2 dark:bg-slate-400 bg-slate-100 px-2 block h-10 rounded-md sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
placeholder="Port"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row mt-5">
|
||||||
|
<div>
|
||||||
|
<label className="block py-1">Username</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Field
|
||||||
|
name="username"
|
||||||
|
component="input"
|
||||||
|
className="h-10 dark:bg-slate-500 bg-slate-200 rounded-md text-gray-700 dark:text-slate-200 py-1 px-10 mr-5 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
<span className="absolute h-6 w-6 left-2 top-2 inset-y-0 flex items-center px-0 pointer-events-none">
|
||||||
|
<i className="icon-[solar--user-bold-duotone] h-6 w-6 dark:text-slate-200" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label className="block py-1">Password</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Field
|
||||||
|
name="password"
|
||||||
|
component="input"
|
||||||
|
type="password"
|
||||||
|
className="h-10 dark:bg-slate-500 bg-slate-200 rounded-md text-gray-700 dark:text-slate-200 py-1 px-10 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<span className="absolute left-2 top-2 inset-y-0 flex items-center px-0 pointer-events-none h-6 w-6">
|
||||||
|
<i className="icon-[solar--lock-password-bold-duotone] h-6 w-6 dark:text-slate-200" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<button
|
||||||
|
className="flex space-x-1 sm:mt-5 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-4 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span className="text-md">
|
||||||
|
{!isEmpty(initialData) ? "Update" : "Save"}
|
||||||
|
</span>
|
||||||
|
<span className="w-6 h-6">
|
||||||
|
<i className="h-6 w-6 icon-[solar--diskette-bold-duotone]"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex space-x-1 sm:mt-5 sm:flex-row sm:items-center rounded-lg border border-red-400 dark:border-red-200 bg-red-200 px-4 py-2 text-gray-500 hover:bg-transparent hover:text-red-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
>
|
||||||
|
{!isEmpty(initialData) && "Delete"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
5
src/client/components/shared/ErrorPage.tsx
Normal file
5
src/client/components/shared/ErrorPage.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const ErrorPage = () => {
|
||||||
|
return <>Error has been encountered.</>;
|
||||||
|
};
|
||||||
20
src/client/components/shared/Header.tsx
Normal file
20
src/client/components/shared/Header.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { ReactElement } from "react";
|
||||||
|
|
||||||
|
type IHeaderProps = {
|
||||||
|
headerContent: string;
|
||||||
|
subHeaderContent: string;
|
||||||
|
iconClassNames: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Header = (props: IHeaderProps): ReactElement => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="title is-4">
|
||||||
|
<i className={props.iconClassNames}></i> {props.headerContent}
|
||||||
|
</h4>
|
||||||
|
<p className="subtitle is-7">{props.subHeaderContent}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -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";
|
||||||
@@ -31,55 +31,53 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
{
|
{
|
||||||
name: "rawFileDetails",
|
name: "rawFileDetails",
|
||||||
content: () => (
|
content: () => (
|
||||||
<dl>
|
<dl className="dark:bg-[#647587] bg-slate-200 p-3 rounded-lg">
|
||||||
<dt>
|
<dt>
|
||||||
<h6
|
<p className="text-lg">{issueName}</p>
|
||||||
className="name has-text-weight-medium mb-1"
|
|
||||||
style={props.titleStyle}
|
|
||||||
>
|
|
||||||
{issueName}
|
|
||||||
</h6>
|
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="is-size-7">
|
<dd className="text-sm">
|
||||||
Is a part of{" "}
|
is a part of{" "}
|
||||||
<span className="has-text-weight-semibold">
|
<span className="underline">
|
||||||
{inferredMetadata.issue.name}
|
{inferredMetadata.issue.name}
|
||||||
|
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dd className="is-size-7 mt-2">
|
{/* Issue number */}
|
||||||
<div className="field is-grouped is-grouped-multiline">
|
{inferredMetadata.issue.number && (
|
||||||
<div className="control">
|
<dd className="my-2">
|
||||||
<span className="tags">
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<span
|
<span className="pr-1 pt-1">
|
||||||
className="tag is-success is-light has-text-weight-semibold"
|
<i className="icon-[solar--hashtag-outline] w-3.5 h-3.5"></i>
|
||||||
style={props.tagsStyle}
|
|
||||||
>
|
|
||||||
{rawFileDetails.extension}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="tag is-success is-light"
|
|
||||||
style={props.tagsStyle}
|
|
||||||
>
|
|
||||||
{prettyBytes(rawFileDetails.fileSize)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
<div className="control">
|
{inferredMetadata.issue.number}
|
||||||
{inferredMetadata.issue.number && (
|
</span>
|
||||||
<div className="tags has-addons">
|
</span>
|
||||||
<span className="tag is-light" style={props.tagsStyle}>
|
</dd>
|
||||||
Issue #
|
)}
|
||||||
</span>
|
<dd className="flex flex-row gap-2 w-max">
|
||||||
<span className="tag is-warning" style={props.tagsStyle}>
|
{/* File extension */}
|
||||||
{inferredMetadata.issue.number}
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
</span>
|
<span className="pr-1 pt-1">
|
||||||
</div>
|
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
|
||||||
)}
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
|
{rawFileDetails.mimeType}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* size */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
|
{prettyBytes(rawFileDetails.fileSize)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
),
|
),
|
||||||
@@ -202,25 +200,14 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="column" style={props.containerStyle}>
|
<div className="flex gap-5 my-3">
|
||||||
<div className="comic-detail issue-metadata">
|
<Card
|
||||||
<dl>
|
imageUrl={url}
|
||||||
<dd>
|
orientation={"cover-only"}
|
||||||
<div className="columns mt-2">
|
hasDetails={false}
|
||||||
<div className="column is-3">
|
imageStyle={props.imageStyle}
|
||||||
<Card
|
/>
|
||||||
imageUrl={url}
|
<div>{metadataPanel.content()}</div>
|
||||||
orientation={"vertical"}
|
|
||||||
hasDetails={false}
|
|
||||||
imageStyle={props.imageStyle}
|
|
||||||
// cardContainerStyle={{ maxWidth: 200 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="column">{metadataPanel.content()}</div>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
import React from "react";
|
import React 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 { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import { isUndefined } from "lodash";
|
import { format, fromUnixTime } from "date-fns";
|
||||||
|
import { useStore } from "../../store/index";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
const Navbar: React.FunctionComponent = (props) => {
|
const Navbar: React.FunctionComponent = (props) => {
|
||||||
const downloadProgressTick = useSelector(
|
const {
|
||||||
(state: RootState) => state.airdcpp.downloadProgressData,
|
airDCPPSocketConnected,
|
||||||
|
airDCPPDisconnectionInfo,
|
||||||
|
airDCPPSessionInformation,
|
||||||
|
airDCPPDownloadTick,
|
||||||
|
importJobQueue,
|
||||||
|
} = useStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
airDCPPSocketConnected: state.airDCPPSocketConnected,
|
||||||
|
airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo,
|
||||||
|
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
||||||
|
airDCPPDownloadTick: state.airDCPPDownloadTick,
|
||||||
|
importJobQueue: state.importJobQueue,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
// const downloadProgressTick = useSelector(
|
||||||
|
// (state: RootState) => state.airdcpp.downloadProgressData,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const airDCPPSocketConnectionStatus = useSelector(
|
||||||
|
// (state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
|
||||||
|
// );
|
||||||
|
// const airDCPPSessionInfo = useSelector(
|
||||||
|
// (state: RootState) => state.airdcpp.airDCPPSessionInfo,
|
||||||
|
// );
|
||||||
|
// const socketDisconnectionReason = useSelector(
|
||||||
|
// (state: RootState) => state.airdcpp.socketDisconnectionReason,
|
||||||
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar is-fixed-top">
|
<nav className="navbar is-fixed-top">
|
||||||
<div className="navbar-brand">
|
<div className="navbar-brand">
|
||||||
<Link to="/" className="navbar-item">
|
<Link to="/dashboard" className="navbar-item">
|
||||||
<img
|
<img
|
||||||
src="/img/threetwo.svg"
|
src="/src/client/assets/img/threetwo.svg"
|
||||||
alt="ThreeTwo! A comic book curator"
|
alt="ThreeTwo! A comic book curator"
|
||||||
width="112"
|
width="112"
|
||||||
height="28"
|
height="28"
|
||||||
@@ -59,7 +86,7 @@ const Navbar: React.FunctionComponent = (props) => {
|
|||||||
Downloads
|
Downloads
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<SearchBar />
|
{/* <SearchBar /> */}
|
||||||
|
|
||||||
<Link to="/search" className="navbar-item">
|
<Link to="/search" className="navbar-item">
|
||||||
Search ComicVine
|
Search ComicVine
|
||||||
@@ -72,26 +99,100 @@ const Navbar: React.FunctionComponent = (props) => {
|
|||||||
<div className="navbar-item has-dropdown is-hoverable">
|
<div className="navbar-item has-dropdown is-hoverable">
|
||||||
<a className="navbar-link is-arrowless">
|
<a className="navbar-link is-arrowless">
|
||||||
<i className="fa-solid fa-download"></i>
|
<i className="fa-solid fa-download"></i>
|
||||||
{downloadProgressTick && (
|
{!isEmpty(airDCPPDownloadTick) && (
|
||||||
<div className="pulsating-circle"></div>
|
<div className="pulsating-circle"></div>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
{!isUndefined(downloadProgressTick) ? (
|
{!isEmpty(airDCPPDownloadTick) ? (
|
||||||
<div className="navbar-dropdown download-progress-meter">
|
<div className="navbar-dropdown is-right is-boxed">
|
||||||
<a className="navbar-item">
|
<a className="navbar-item">
|
||||||
<DownloadProgressTick data={downloadProgressTick} />
|
<DownloadProgressTick data={airDCPPDownloadTick} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isUndefined(importJobQueue.status) &&
|
||||||
|
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>
|
||||||
|
{importJobQueue.successfulJobCount > 0 ? (
|
||||||
|
<li className="mb-2">
|
||||||
|
<span className="tag is-success mr-2">
|
||||||
|
{importJobQueue.successfulJobCount}
|
||||||
|
</span>
|
||||||
|
imported.
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{importJobQueue.failedJobCount > 0 ? (
|
||||||
|
<li>
|
||||||
|
<span className="tag is-danger mr-2">
|
||||||
|
{importJobQueue.failedJobCount}
|
||||||
|
</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">
|
<div className="navbar-item has-dropdown is-hoverable">
|
||||||
<a className="navbar-link is-arrowless">
|
{!isUndefined(airDCPPSessionInformation.user) ? (
|
||||||
<i className="fa-solid fa-bolt"></i>
|
<>
|
||||||
</a>
|
<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(
|
||||||
|
airDCPPSessionInformation?.user.last_login,
|
||||||
|
),
|
||||||
|
"dd MMMM, yyyy",
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<hr className="navbar-divider" />
|
||||||
|
<p>
|
||||||
|
<span className="tag has-text-success">
|
||||||
|
{airDCPPSessionInformation.user.username}
|
||||||
|
</span>
|
||||||
|
connected to{" "}
|
||||||
|
<span className="tag has-text-success">
|
||||||
|
{airDCPPSessionInformation.system_info.client_version}
|
||||||
|
</span>{" "}
|
||||||
|
with session ID{" "}
|
||||||
|
<span className="tag has-text-success">
|
||||||
|
{airDCPPSessionInformation.session_id}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</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(airDCPPDisconnectionInfo, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</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>
|
||||||
<div id="blogDropdown" className="navbar-dropdown">
|
<div id="blogDropdown" className="navbar-dropdown">
|
||||||
121
src/client/components/shared/Navbar2.tsx
Normal file
121
src/client/components/shared/Navbar2.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { ReactElement, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useDarkMode } from "../../hooks/useDarkMode";
|
||||||
|
|
||||||
|
export const Navbar2 = (): ReactElement => {
|
||||||
|
const [colorTheme, setTheme] = useDarkMode();
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
|
||||||
|
const toggleDarkMode = (checked) => {
|
||||||
|
setTheme(colorTheme);
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="bg-white dark:bg-gray-900 gap-8 px-5 py-5 h-18 border-b-2 border-gray-300 dark:border-slate-200">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="mx-auto flex">
|
||||||
|
<img src="/src/client/assets/img/threetwo.png" alt="ThreeTwo!" />
|
||||||
|
|
||||||
|
{/* Main Navigation */}
|
||||||
|
<div className="flex flex-1 items-center justify-end md:justify-between">
|
||||||
|
<nav
|
||||||
|
aria-label="ThreeTwo Main Navigation"
|
||||||
|
className="hidden md:block"
|
||||||
|
>
|
||||||
|
<ul className="flex items-center gap-6 text-md">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/import"
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/library"
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
Library
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Volumes
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Downloads
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Right-most Nav */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<ul className="flex items-center gap-6 text-md">
|
||||||
|
{/* Settings Icon and text */}
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/settings"
|
||||||
|
className="flex items-center space-x-1 text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75"
|
||||||
|
>
|
||||||
|
<span className="w-5 h-5">
|
||||||
|
<i className="icon-[solar--settings-outline] h-5 w-5"></i>
|
||||||
|
</span>
|
||||||
|
<span>Settings</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{/* Light/Dark Mode toggle */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-gray-600 dark:text-white">Light</span>
|
||||||
|
<label
|
||||||
|
htmlFor="toggle"
|
||||||
|
className="relative inline-flex items-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="toggle"
|
||||||
|
className="sr-only"
|
||||||
|
checked={darkMode}
|
||||||
|
onChange={toggleDarkMode}
|
||||||
|
/>
|
||||||
|
<span className="bg-gray-300 w-10 h-6 rounded-full"></span>
|
||||||
|
<span
|
||||||
|
className={`bg-white w-4 h-4 rounded-full absolute left-1 top-1 transition-transform duration-300 ease-in-out ${
|
||||||
|
darkMode ? "translate-x-4" : ""
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</label>
|
||||||
|
<span className="text-gray-600 dark:text-white">Dark</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,28 +12,30 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
export const T2Table = (tableOptions): ReactElement => {
|
export const T2Table = (tableOptions): ReactElement => {
|
||||||
const { sourceData, columns, paginationHandlers: { nextPage, previousPage }, totalPages, rowClickHandler } =
|
const {
|
||||||
tableOptions;
|
sourceData,
|
||||||
|
columns,
|
||||||
const [{ pageIndex, pageSize }, setPagination] =
|
paginationHandlers: { nextPage, previousPage },
|
||||||
useState<PaginationState>({
|
totalPages,
|
||||||
pageIndex: 1,
|
rowClickHandler,
|
||||||
pageSize: 15,
|
} = tableOptions;
|
||||||
});
|
|
||||||
console.log(sourceData)
|
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 1,
|
||||||
|
pageSize: 15,
|
||||||
|
});
|
||||||
|
|
||||||
const pagination = useMemo(
|
const pagination = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
}),
|
}),
|
||||||
[pageIndex, pageSize]
|
[pageIndex, pageSize],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination control to move forward one page
|
* Pagination control to move forward one page
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
const goToNextPage = () => {
|
const goToNextPage = () => {
|
||||||
setPagination({
|
setPagination({
|
||||||
@@ -41,10 +43,10 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
nextPage(pageIndex, pageSize);
|
nextPage(pageIndex, pageSize);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination control to move backward one page
|
* Pagination control to move backward one page
|
||||||
* @returns void
|
* @returns void
|
||||||
**/
|
**/
|
||||||
const goToPreviousPage = () => {
|
const goToPreviousPage = () => {
|
||||||
@@ -53,7 +55,7 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
previousPage(pageIndex, pageSize);
|
previousPage(pageIndex, pageSize);
|
||||||
}
|
};
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: sourceData,
|
data: sourceData,
|
||||||
@@ -68,62 +70,52 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="container max-w-fit mx-14">
|
||||||
<div className="columns table-controls">
|
<div>
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
<div className="column is-half">
|
<div className="flex flex-row gap-2 justify-between mt-5">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
|
||||||
{/* pagination controls */}
|
|
||||||
<nav className="pagination columns">
|
|
||||||
<div className="mr-4 has-text-weight-semibold has-text-left">
|
|
||||||
<p className="is-size-5">Page {pageIndex} of {Math.ceil(totalPages / pageSize)}</p>
|
|
||||||
{/* <p>{totalPages} comics in all</p> */}
|
|
||||||
</div>
|
|
||||||
<div className="field has-addons">
|
|
||||||
<p className="control">
|
|
||||||
<div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div>
|
|
||||||
</p>
|
|
||||||
<p className="control">
|
|
||||||
<div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="field has-addons ml-5">
|
{/* pagination controls */}
|
||||||
<p className="control">
|
<div>
|
||||||
<button className="button">
|
Page {pageIndex} of {Math.ceil(totalPages / pageSize)}
|
||||||
<span className="icon is-small">
|
<p>{totalPages} comics in all</p>
|
||||||
<i className="fa-solid fa-list"></i>
|
{/* Prev/Next buttons */}
|
||||||
</span>
|
<div className="inline-flex flex-row mt-4 mb-4">
|
||||||
</button>
|
<button
|
||||||
</p>
|
onClick={() => goToPreviousPage()}
|
||||||
<p className="control">
|
disabled={pageIndex === 1}
|
||||||
<button className="button">
|
className="dark:bg-slate-500 bg-slate-400 rounded-l border-slate-600 border-r pt-2 px-2"
|
||||||
<Link to="/library-grid">
|
>
|
||||||
<span className="icon is-small">
|
<i className="icon-[solar--arrow-left-linear] h-6 w-6"></i>
|
||||||
<i className="fa-solid fa-image"></i>
|
</button>
|
||||||
</span>
|
<button
|
||||||
</Link>
|
className="dark:bg-slate-500 bg-slate-400 rounded-r pt-2 px-2"
|
||||||
</button>
|
onClick={() => goToNextPage()}
|
||||||
</p>
|
disabled={pageIndex > Math.floor(totalPages / pageSize)}
|
||||||
|
>
|
||||||
|
<i className="icon-[solar--arrow-right-linear] h-6 w-6"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table className="table is-hoverable">
|
<table className="table-auto overflow-auto">
|
||||||
<thead>
|
<thead className="sticky top-0 bg-slate-200 dark:bg-slate-500">
|
||||||
{table.getHeaderGroups().map((headerGroup, idx) => (
|
{table.getHeaderGroups().map((headerGroup, idx) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header, idx) => (
|
{headerGroup.headers.map((header, idx) => (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
|
className="px-3 py-3"
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -133,21 +125,23 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row, idx) => {
|
{table.getRowModel().rows.map((row, idx) => {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr key={row.id} onClick={() => rowClickHandler(row)}>
|
||||||
key={row.id}
|
{row.getVisibleCells().map((cell) => {
|
||||||
onClick={() => rowClickHandler(row)}
|
return (
|
||||||
>
|
<td key={cell.id} className="align-top">
|
||||||
{row.getVisibleCells().map(cell => (
|
{flexRender(
|
||||||
<td key={cell.id}>
|
cell.column.columnDef.cell,
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
cell.getContext(),
|
||||||
</td>
|
)}
|
||||||
))}
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const IMS_RAW_IMPORT_FAILED = "IMS_RAW_IMPORT_FAILED";
|
|||||||
export const LS_IMPORT_CALL_IN_PROGRESS = "LS_IMPORT_CALL_IN_PROGRESS";
|
export const LS_IMPORT_CALL_IN_PROGRESS = "LS_IMPORT_CALL_IN_PROGRESS";
|
||||||
// Library import bull mq queue control
|
// Library import bull mq queue control
|
||||||
export const LS_TOGGLE_IMPORT_QUEUE = "LS_TOGGLE_IMPORT_QUEUE";
|
export const LS_TOGGLE_IMPORT_QUEUE = "LS_TOGGLE_IMPORT_QUEUE";
|
||||||
export const LS_QUEUE_DRAINED = "LS_QUEUE_DRAINED";
|
|
||||||
|
|
||||||
// ComicVine Metadata
|
// ComicVine Metadata
|
||||||
export const IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS =
|
export const IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS =
|
||||||
@@ -51,11 +50,12 @@ export const IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS =
|
|||||||
"IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS";
|
"IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS";
|
||||||
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
|
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
|
||||||
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
|
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
|
||||||
export const VOLUMES_FETCHED="VOLUMES_FETCHED";
|
export const VOLUMES_FETCHED = "VOLUMES_FETCHED";
|
||||||
|
|
||||||
// search results from the Search service
|
// search results from the Search service
|
||||||
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";
|
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";
|
||||||
export const SS_SEARCH_RESULTS_FETCHED_SPECIAL = "SS_SEARCH_RESULTS_FETCHED_SPECIAL";
|
export const SS_SEARCH_RESULTS_FETCHED_SPECIAL =
|
||||||
|
"SS_SEARCH_RESULTS_FETCHED_SPECIAL";
|
||||||
export const SS_SEARCH_IN_PROGRESS = "SS_SEARCH_IN_PROGRESS";
|
export const SS_SEARCH_IN_PROGRESS = "SS_SEARCH_IN_PROGRESS";
|
||||||
export const SS_SEARCH_FAILED = "SS_SEARCH_FAILED";
|
export const SS_SEARCH_FAILED = "SS_SEARCH_FAILED";
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export const AIRDCPP_SEARCH_RESULTS_UPDATED = "AIRDCPP_SEARCH_RESULTS_UPDATED";
|
|||||||
export const AIRDCPP_SEARCH_COMPLETE = "AIRDCPP_SEARCH_COMPLETE";
|
export const AIRDCPP_SEARCH_COMPLETE = "AIRDCPP_SEARCH_COMPLETE";
|
||||||
|
|
||||||
// AirDC++ related library query for issues with bundles associated with them
|
// AirDC++ related library query for issues with bundles associated with them
|
||||||
export const LIBRARY_ISSUE_BUNDLES = "LIBRARY_ISSUE_BUNDLES";
|
export const LIBRARY_ISSUE_BUNDLES = "LIBRARY_ISSUE_BUNDLES";
|
||||||
|
|
||||||
export const AIRDCPP_HUB_SEARCHES_SENT = "AIRDCPP_HUB_SEARCHES_SENT";
|
export const AIRDCPP_HUB_SEARCHES_SENT = "AIRDCPP_HUB_SEARCHES_SENT";
|
||||||
export const AIRDCPP_RESULT_DOWNLOAD_INITIATED =
|
export const AIRDCPP_RESULT_DOWNLOAD_INITIATED =
|
||||||
@@ -115,6 +115,8 @@ export const AIRDCPP_FILE_DOWNLOAD_COMPLETED =
|
|||||||
export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
|
export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
|
||||||
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
|
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
|
||||||
export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK";
|
export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK";
|
||||||
|
export const AIRDCPP_SOCKET_CONNECTED = "AIRDCPP_SOCKET_CONNECTED";
|
||||||
|
export const AIRDCPP_SOCKET_DISCONNECTED = "AIRDCPP_SOCKET_DISCONNECTED";
|
||||||
|
|
||||||
// Transfers
|
// Transfers
|
||||||
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
|
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
|
||||||
@@ -122,10 +124,17 @@ export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
|
|||||||
// Comics marked as "wanted"
|
// Comics marked as "wanted"
|
||||||
export const WANTED_COMICS_FETCHED = "WANTED_COMICS_FETCHED";
|
export const WANTED_COMICS_FETCHED = "WANTED_COMICS_FETCHED";
|
||||||
|
|
||||||
// LIBRARY SOCKET ENDPOINT
|
// LIBRARY Service import queue-related action types
|
||||||
export const LS_IMPORT = "LS_IMPORT";
|
export const LS_IMPORT = "LS_IMPORT";
|
||||||
export const LS_COVER_EXTRACTED = "LS_COVER_EXTRACTED";
|
export const LS_COVER_EXTRACTED = "LS_COVER_EXTRACTED";
|
||||||
|
export const LS_COVER_EXTRACTION_FAILED = "LS_COVER_EXTRACTION_FAILED";
|
||||||
export const LS_COMIC_ADDED = "LS_COMIC_ADDED";
|
export const LS_COMIC_ADDED = "LS_COMIC_ADDED";
|
||||||
|
export const LS_IMPORT_QUEUE_DRAINED = "LS_IMPORT_QUEUE_DRAINED";
|
||||||
|
export const LS_SET_QUEUE_STATUS = "LS_SET_QUEUE_STATUS";
|
||||||
|
export const RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION =
|
||||||
|
"RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION";
|
||||||
|
export const LS_IMPORT_JOB_STATISTICS_FETCHED =
|
||||||
|
"LS_IMPORT_JOB_STATISTICS_FETCHED";
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const SETTINGS_CALL_IN_PROGRESS = "SETTINGS_CALL_IN_PROGRESS";
|
export const SETTINGS_CALL_IN_PROGRESS = "SETTINGS_CALL_IN_PROGRESS";
|
||||||
@@ -134,7 +143,12 @@ 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";
|
||||||
export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR";
|
export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR";
|
||||||
|
|
||||||
|
// service health statuses
|
||||||
|
export const LIBRARY_SERVICE_HEALTH = "LIBRARY_SERVICE_HEALTH";
|
||||||
|
|||||||
@@ -8,71 +8,86 @@ export const hostURIBuilder = (options: Record<string, string>): string => {
|
|||||||
options.apiPath
|
options.apiPath
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
console.log(import.meta);
|
||||||
|
|
||||||
export const CORS_PROXY_SERVER_URI = hostURIBuilder({
|
export const CORS_PROXY_SERVER_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "8050",
|
port: "8050",
|
||||||
apiPath: "/",
|
apiPath: "/",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const COMICVINE_SERVICE_URI = hostURIBuilder({
|
export const COMICVINE_SERVICE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3080",
|
port: "3080",
|
||||||
apiPath: "/api/comicvine",
|
apiPath: "/api/comicvine",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const METRON_SERVICE_URI = hostURIBuilder({
|
export const METRON_SERVICE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3080",
|
port: "3080",
|
||||||
apiPath: "/api/metron",
|
apiPath: "/api/metron",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const API_BASE_URI = hostURIBuilder({
|
export const API_BASE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "8050",
|
port: "8050",
|
||||||
apiPath: "/api",
|
apiPath: "/api",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LIBRARY_SERVICE_HOST = hostURIBuilder({
|
export const LIBRARY_SERVICE_HOST = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3000",
|
port: "3000",
|
||||||
apiPath: ``,
|
apiPath: ``,
|
||||||
});
|
});
|
||||||
export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({
|
export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3000",
|
port: "3000",
|
||||||
apiPath: "/api/library",
|
apiPath: "/api/library",
|
||||||
});
|
});
|
||||||
export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({
|
export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3000",
|
port: "3000",
|
||||||
apiPath: "/api/search",
|
apiPath: "/api/search",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({
|
export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3000",
|
port: "3000",
|
||||||
apiPath: "/api/settings",
|
apiPath: "/api/settings",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({
|
export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3000",
|
port: "3000",
|
||||||
apiPath: "/api/imagetransformation",
|
apiPath: "/api/imagetransformation",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SOCKET_BASE_URI = hostURIBuilder({
|
export const SOCKET_BASE_URI = hostURIBuilder({
|
||||||
protocol: "ws",
|
protocol: "ws",
|
||||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||||
port: "3001",
|
port: "3001",
|
||||||
apiPath: `/`,
|
apiPath: `/`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const JOB_QUEUE_SERVICE_BASE_URI = hostURIBuilder({
|
||||||
|
protocol: "http",
|
||||||
|
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
|
||||||
|
port: "3000",
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -57,6 +85,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
"category": "statuses",
|
||||||
|
"displayName": "Service Status",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "core-service",
|
||||||
|
"displayName": "Core Services"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "metadata-service",
|
||||||
|
"displayName": "Metadata Services"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
"category": "system",
|
"category": "system",
|
||||||
"displayName": "System",
|
"displayName": "System",
|
||||||
"children": [
|
"children": [
|
||||||
@@ -67,7 +110,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 6,
|
||||||
"category": "acknowledgments",
|
"category": "acknowledgments",
|
||||||
"displayName": "Acknowledgments",
|
"displayName": "Acknowledgments",
|
||||||
"children": [
|
"children": [
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { isEmpty, isUndefined } from "lodash";
|
|
||||||
import React, { createContext, useEffect, useState } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { getSettings } from "../actions/settings.actions";
|
|
||||||
import AirDCPPSocket from "../services/DcppSearchService";
|
|
||||||
|
|
||||||
const AirDCPPSocketContextProvider = ({ children }) => {
|
|
||||||
// setter for settings for use in the context consumer
|
|
||||||
const setSettings = (settingsObject) => {
|
|
||||||
persistSettings({
|
|
||||||
...airDCPPState,
|
|
||||||
airDCPPState: {
|
|
||||||
settings: settingsObject,
|
|
||||||
socket: {},
|
|
||||||
socketConectionInformation: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// 1. default zero-state for AirDC++ configuration
|
|
||||||
const initState = {
|
|
||||||
airDCPPState: {
|
|
||||||
settings: {},
|
|
||||||
socket: {},
|
|
||||||
socketConnectionInformation: {},
|
|
||||||
},
|
|
||||||
setSettings: setSettings,
|
|
||||||
};
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [airDCPPState, persistSettings] = useState(initState);
|
|
||||||
const airDCPPSettings = useSelector(
|
|
||||||
(state: RootState) => state.settings.data,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 1. get settings from mongo
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(getSettings());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 2. If available, init AirDC++ Socket with those settings
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isEmpty(airDCPPSettings)) {
|
|
||||||
initializeAirDCPPSocket(airDCPPSettings);
|
|
||||||
}
|
|
||||||
}, [airDCPPSettings]);
|
|
||||||
|
|
||||||
// Method to init AirDC++ Socket with supplied settings
|
|
||||||
const initializeAirDCPPSocket = async (configuration) => {
|
|
||||||
console.log("[AirDCPP]: Initializing socket...");
|
|
||||||
const {
|
|
||||||
directConnect: {
|
|
||||||
client: { host },
|
|
||||||
},
|
|
||||||
} = configuration;
|
|
||||||
const initializedAirDCPPSocket = new AirDCPPSocket({
|
|
||||||
protocol: `${host.protocol}`,
|
|
||||||
hostname: `${host.hostname}:${host.port}`,
|
|
||||||
});
|
|
||||||
const socketConnectionInformation = await initializedAirDCPPSocket.connect(
|
|
||||||
`${host.username}`,
|
|
||||||
`${host.password}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
persistSettings({
|
|
||||||
...airDCPPState,
|
|
||||||
airDCPPState: {
|
|
||||||
settings: configuration,
|
|
||||||
socket: initializedAirDCPPSocket,
|
|
||||||
socketConnectionInformation,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// the Provider gives access to the context to its children
|
|
||||||
return (
|
|
||||||
<AirDCPPSocketContext.Provider value={airDCPPState}>
|
|
||||||
{children}
|
|
||||||
</AirDCPPSocketContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const AirDCPPSocketContext = createContext({
|
|
||||||
airDCPPState: {},
|
|
||||||
saveSettings: () => { },
|
|
||||||
});
|
|
||||||
|
|
||||||
export { AirDCPPSocketContext, AirDCPPSocketContextProvider };
|
|
||||||
11
src/client/context/SocketIOContext.tsx
Normal file
11
src/client/context/SocketIOContext.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React, { createContext } from "react";
|
||||||
|
|
||||||
|
export const SocketIOContext = createContext({});
|
||||||
|
|
||||||
|
export const SocketIOProvider = ({ children, socket }) => {
|
||||||
|
return (
|
||||||
|
<SocketIOContext.Provider value={socket}>
|
||||||
|
{children}
|
||||||
|
</SocketIOContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
17
src/client/hooks/useDarkMode.tsx
Normal file
17
src/client/hooks/useDarkMode.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const useDarkMode = () => {
|
||||||
|
const [theme, setTheme] = useState(localStorage.theme);
|
||||||
|
const colorTheme = theme === "dark" ? "light" : "dark";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
root.classList.remove(colorTheme);
|
||||||
|
root.classList.add(theme);
|
||||||
|
|
||||||
|
// save theme to local storage
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}, [theme, colorTheme]);
|
||||||
|
|
||||||
|
return [colorTheme, setTheme];
|
||||||
|
};
|
||||||
@@ -1,18 +1,45 @@
|
|||||||
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";
|
||||||
|
import { ErrorPage } from "./components/shared/ErrorPage";
|
||||||
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";
|
||||||
|
import Import from "./components/Import/Import";
|
||||||
|
import Dashboard from "./components/Dashboard/Dashboard";
|
||||||
|
import TabulatedContentContainer from "./components/Library/TabulatedContentContainer";
|
||||||
|
import { ComicDetailContainer } from "./components/ComicDetail/ComicDetailContainer";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <App />,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{ path: "dashboard", element: <Dashboard /> },
|
||||||
|
{ path: "settings", element: <Settings /> },
|
||||||
|
{
|
||||||
|
path: "library",
|
||||||
|
element: <TabulatedContentContainer category="library" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "comic/details/:comicObjectId",
|
||||||
|
element: <ComicDetailContainer />,
|
||||||
|
},
|
||||||
|
{ path: "import", element: <Import path={"./comics"} /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router history={history}>
|
<RouterProvider router={router} />
|
||||||
<App />
|
<ReactQueryDevtools initialIsOpen={true} />
|
||||||
</Router>
|
</QueryClientProvider>,
|
||||||
</Provider>,
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
AIRDCPP_BUNDLES_FETCHED,
|
AIRDCPP_BUNDLES_FETCHED,
|
||||||
AIRDCPP_TRANSFERS_FETCHED,
|
AIRDCPP_TRANSFERS_FETCHED,
|
||||||
LIBRARY_ISSUE_BUNDLES,
|
LIBRARY_ISSUE_BUNDLES,
|
||||||
|
AIRDCPP_SOCKET_CONNECTED,
|
||||||
|
AIRDCPP_SOCKET_DISCONNECTED,
|
||||||
} from "../constants/action-types";
|
} from "../constants/action-types";
|
||||||
import { LOCATION_CHANGE } from "redux-first-history";
|
import { LOCATION_CHANGE } from "redux-first-history";
|
||||||
import { isNil, isUndefined } from "lodash";
|
import { isNil, isUndefined } from "lodash";
|
||||||
@@ -24,6 +26,9 @@ const initialState = {
|
|||||||
downloadFileStatus: {},
|
downloadFileStatus: {},
|
||||||
bundles: [],
|
bundles: [],
|
||||||
transfers: [],
|
transfers: [],
|
||||||
|
isAirDCPPSocketConnected: false,
|
||||||
|
airDCPPSessionInfo: {},
|
||||||
|
socketDisconnectionReason: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function airdcppReducer(state = initialState, action) {
|
function airdcppReducer(state = initialState, action) {
|
||||||
@@ -95,8 +100,23 @@ function airdcppReducer(state = initialState, action) {
|
|||||||
...state,
|
...state,
|
||||||
transfers: action.bundles,
|
transfers: action.bundles,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case AIRDCPP_SOCKET_CONNECTED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isAirDCPPSocketConnected: true,
|
||||||
|
airDCPPSessionInfo: action.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
case AIRDCPP_SOCKET_DISCONNECTED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isAirDCPPSocketConnected: false,
|
||||||
|
socketDisconnectionReason: action.data,
|
||||||
|
};
|
||||||
case LOCATION_CHANGE:
|
case LOCATION_CHANGE:
|
||||||
return {
|
return {
|
||||||
|
...state,
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
isAirDCPPSearchInProgress: false,
|
isAirDCPPSearchInProgress: false,
|
||||||
searchInfo: null,
|
searchInfo: null,
|
||||||
@@ -105,7 +125,6 @@ function airdcppReducer(state = initialState, action) {
|
|||||||
bundleDBImportResult: null,
|
bundleDBImportResult: null,
|
||||||
// bundles: [],
|
// bundles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import {
|
|||||||
IMS_COMIC_BOOK_GROUPS_FETCHED,
|
IMS_COMIC_BOOK_GROUPS_FETCHED,
|
||||||
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
|
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
|
||||||
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
||||||
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
|
|
||||||
LS_IMPORT,
|
LS_IMPORT,
|
||||||
LS_COVER_EXTRACTED,
|
LS_COVER_EXTRACTED,
|
||||||
LS_QUEUE_DRAINED,
|
LS_COVER_EXTRACTION_FAILED,
|
||||||
LS_COMIC_ADDED,
|
LS_COMIC_ADDED,
|
||||||
IMG_ANALYSIS_CALL_IN_PROGRESS,
|
IMG_ANALYSIS_CALL_IN_PROGRESS,
|
||||||
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
||||||
@@ -30,6 +29,11 @@ import {
|
|||||||
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||||
VOLUMES_FETCHED,
|
VOLUMES_FETCHED,
|
||||||
COMICBOOK_EXTRACTION_SUCCESS,
|
COMICBOOK_EXTRACTION_SUCCESS,
|
||||||
|
LIBRARY_SERVICE_HEALTH,
|
||||||
|
LS_IMPORT_QUEUE_DRAINED,
|
||||||
|
LS_SET_QUEUE_STATUS,
|
||||||
|
RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION,
|
||||||
|
LS_IMPORT_JOB_STATISTICS_FETCHED,
|
||||||
} from "../constants/action-types";
|
} from "../constants/action-types";
|
||||||
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
|
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
|
||||||
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
|
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
|
||||||
@@ -40,6 +44,7 @@ const initialState = {
|
|||||||
SSCallInProgress: false,
|
SSCallInProgress: false,
|
||||||
imageAnalysisResults: {},
|
imageAnalysisResults: {},
|
||||||
comicBookExtractionInProgress: false,
|
comicBookExtractionInProgress: false,
|
||||||
|
LSQueueImportStatus: undefined,
|
||||||
comicBookMetadata: [],
|
comicBookMetadata: [],
|
||||||
comicVolumeGroups: [],
|
comicVolumeGroups: [],
|
||||||
isSocketConnected: false,
|
isSocketConnected: false,
|
||||||
@@ -55,11 +60,14 @@ const initialState = {
|
|||||||
libraryComics: [],
|
libraryComics: [],
|
||||||
volumes: [],
|
volumes: [],
|
||||||
librarySearchResultsFormatted: [],
|
librarySearchResultsFormatted: [],
|
||||||
librarySearchResultCount: 0,
|
lastQueueJob: "",
|
||||||
|
successfulJobCount: 0,
|
||||||
|
failedJobCount: 0,
|
||||||
|
importJobStatistics: [],
|
||||||
libraryQueueResults: [],
|
libraryQueueResults: [],
|
||||||
librarySearchError: {},
|
librarySearchError: {},
|
||||||
|
libraryServiceStatus: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function fileOpsReducer(state = initialState, action) {
|
function fileOpsReducer(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case IMS_COMICBOOK_METADATA_FETCHED:
|
case IMS_COMICBOOK_METADATA_FETCHED:
|
||||||
@@ -149,17 +157,58 @@ function fileOpsReducer(state = initialState, action) {
|
|||||||
case LS_IMPORT: {
|
case LS_IMPORT: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
LSQueueImportStatus: "running",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case LS_COVER_EXTRACTED: {
|
case LS_COVER_EXTRACTED: {
|
||||||
console.log("BASH", action);
|
if (state.recentComics.length === 5) {
|
||||||
if(state.recentComics.length === 5) {
|
|
||||||
state.recentComics.pop();
|
state.recentComics.pop();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
librarySearchResultCount: state.librarySearchResultCount + 1,
|
successfulJobCount: action.completedJobCount,
|
||||||
recentComics: [...state.recentComics, action.result.data.importResult]
|
lastQueueJob: action.importResult.rawFileDetails.name,
|
||||||
|
recentComics: [...state.recentComics, action.importResult],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case LS_COVER_EXTRACTION_FAILED: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
failedJobCount: action.failedJobCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case LS_IMPORT_QUEUE_DRAINED: {
|
||||||
|
localStorage.removeItem("sessionId");
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
LSQueueImportStatus: "drained",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION: {
|
||||||
|
console.log("Restoring state for an active import in progress...");
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
successfulJobCount: action.completedJobCount,
|
||||||
|
failedJobCount: action.failedJobCount,
|
||||||
|
LSQueueImportStatus: action.queueStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case LS_SET_QUEUE_STATUS: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
LSQueueImportStatus: action.data.queueStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case LS_IMPORT_JOB_STATISTICS_FETCHED: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
importJobStatistics: action.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,14 +240,8 @@ function fileOpsReducer(state = initialState, action) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case LS_QUEUE_DRAINED: {
|
|
||||||
console.log("drained", action);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case LS_COMIC_ADDED: {
|
case LS_COMIC_ADDED: {
|
||||||
console.log("ADDED na anna", action);
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
@@ -233,7 +276,7 @@ function fileOpsReducer(state = initialState, action) {
|
|||||||
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
|
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
|
||||||
const foo = [];
|
const foo = [];
|
||||||
if (!isUndefined(action.data.hits)) {
|
if (!isUndefined(action.data.hits)) {
|
||||||
map(action.data.hits.hits, ({ _source }) => {
|
map(action.data.hits, ({ _source }) => {
|
||||||
foo.push(_source);
|
foo.push(_source);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -246,7 +289,7 @@ function fileOpsReducer(state = initialState, action) {
|
|||||||
case WANTED_COMICS_FETCHED: {
|
case WANTED_COMICS_FETCHED: {
|
||||||
const foo = [];
|
const foo = [];
|
||||||
if (!isUndefined(action.data.hits)) {
|
if (!isUndefined(action.data.hits)) {
|
||||||
map(action.data.hits.hits, ({ _source }) => {
|
map(action.data.hits, ({ _source }) => {
|
||||||
foo.push(_source);
|
foo.push(_source);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -271,7 +314,12 @@ function fileOpsReducer(state = initialState, action) {
|
|||||||
SSCallInProgress: false,
|
SSCallInProgress: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case LIBRARY_SERVICE_HEALTH: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
libraryServiceStatus: action.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
case FILEOPS_STATE_RESET: {
|
case FILEOPS_STATE_RESET: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -283,4 +331,4 @@ function fileOpsReducer(state = initialState, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fileOpsReducer;
|
export default fileOpsReducer;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class AirDCPPSocket {
|
|||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
url: `${socketProtocol}://${configuration.hostname}/api/v1/`,
|
url: `${socketProtocol}://${configuration.hostname}/api/v1/`,
|
||||||
autoReconnect: false,
|
autoReconnect: true,
|
||||||
reconnectInterval: 5,
|
reconnectInterval: 5,
|
||||||
logLevel: "verbose",
|
logLevel: "verbose",
|
||||||
ignoredListenerEvents: [
|
ignoredListenerEvents: [
|
||||||
@@ -18,6 +18,8 @@ class AirDCPPSocket {
|
|||||||
"hash_statistics",
|
"hash_statistics",
|
||||||
"hub_counts_updated",
|
"hub_counts_updated",
|
||||||
],
|
],
|
||||||
|
username: `${configuration.username}`,
|
||||||
|
password: `${configuration.password}`,
|
||||||
};
|
};
|
||||||
const AirDCPPSocketInstance = Socket(options, window.WebSocket as any);
|
const AirDCPPSocketInstance = Socket(options, window.WebSocket as any);
|
||||||
return AirDCPPSocketInstance;
|
return AirDCPPSocketInstance;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user