Compare commits
52 Commits
0.0.1-beta
...
80-vite-en
| Author | SHA1 | Date | |
|---|---|---|---|
| 18a0f8621a | |||
| b8fb179ac6 | |||
| 94692bb6d4 | |||
| 4b795aca5d | |||
| fbf6bed4fe | |||
|
|
18b18c3d81 | ||
| a939bf4c71 | |||
| 0a48ecbb2c | |||
| c5dd1abcdd | |||
| 79f9b22fad | |||
|
|
e291b4806b | ||
| ff7b7c9207 | |||
| 2d61fa1436 | |||
| b12849efda | |||
| fb2c969c00 | |||
| d437fddd1a | |||
| 8dd68e9d54 | |||
|
|
a8b52d0ac6 | ||
| 719ebe7c6e | |||
| ddef87ea29 | |||
| 71bad167ab | |||
| df1fbc7dd3 | |||
| 789e5b9518 | |||
| ce6653b5d7 | |||
| d065225d8e | |||
|
|
f854ff9cc6 | ||
| 815444a973 | |||
| 1cbf53be98 | |||
| 0e73f9dad1 | |||
| a15168a6be | |||
|
|
67079f0cb4 | ||
| ef5af01e33 | |||
| 5b6c9c8ffc | |||
| 4556778a47 | |||
| af5f443cbe | |||
| 7babf9f73d | |||
| 1e39daeda2 | |||
| e3cea24615 | |||
| f60c9e4e67 | |||
| 27e6f26331 | |||
| 1f5502ce23 | |||
| 3cb9588bbf | |||
| b1fb256189 | |||
| 74ea2742f0 | |||
| 151c6ec314 | |||
| c6a3be968a | |||
| ff5ce10e17 | |||
| 63e96bf96e | |||
| b699a90a00 | |||
| eda11d3537 | |||
| 262f8d49d7 | |||
| 6b1bb02d57 |
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 = {
|
||||
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",
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
ecmaFeatures: {
|
||||
jsx: true // Allows for the parsing of JSX
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
plugins: ["@typescript-eslint", "css-modules"],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
@@ -18,9 +18,9 @@ module.exports = {
|
||||
},
|
||||
react: {
|
||||
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// Fine tune rules
|
||||
rules: {
|
||||
"@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:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
uses: elgohr/Publish-Docker-Github-Action@v5
|
||||
with:
|
||||
name: frishi/threetwo
|
||||
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",
|
||||
},
|
||||
};
|
||||
17
.storybook/main.ts
Normal file
17
.storybook/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@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;
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:17.3-alpine
|
||||
FROM node:18.15.0-alpine
|
||||
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
|
||||
|
||||
WORKDIR /threetwo
|
||||
@@ -9,11 +9,11 @@ COPY nodemon.json ./
|
||||
COPY jsdoc.json ./
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
COPY . .
|
||||
EXPOSE 3050
|
||||
EXPOSE 5173
|
||||
|
||||
ENTRYPOINT [ "npm", "start" ]
|
||||
15
README.md
15
README.md
@@ -4,6 +4,16 @@ ThreeTwo! _aims to be_ a comic book curation app.
|
||||
|
||||
[](https://github.com/rishighan/threetwo/actions/workflows/docker-image.yml)
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
### 🦄 Early Development Support Channel
|
||||
|
||||
@@ -30,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`
|
||||
2. `yarn run dev` (you can ignore the warnings)
|
||||
3. This will open `http://localhost:3050` 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`
|
||||
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.
|
||||
3. This will open `http://localhost:5173` in your default browser
|
||||
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.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1
funding.yml
Normal file
1
funding.yml
Normal file
@@ -0,0 +1 @@
|
||||
github: [rishighan]
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/client/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
147
package.json
147
package.json
@@ -5,15 +5,12 @@
|
||||
"main": "server/index.js",
|
||||
"typings": "server/index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"start": "npm run build && npm run server",
|
||||
"client": "webpack serve --mode development --devtool inline-source-map --hot",
|
||||
"server": "tsc -p tsconfig.server.json && node server/",
|
||||
"dev": "concurrently \"nodemon\" \"npm run client\"",
|
||||
"server-dev": "nodemon",
|
||||
"build": "vite build",
|
||||
"dev": "rimraf dist && npm run build && vite",
|
||||
"start": "npm run build && vite",
|
||||
"docs": "jsdoc -c jsdoc.json",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"author": "Rishi Ghan",
|
||||
"license": "MIT",
|
||||
@@ -23,79 +20,68 @@
|
||||
"@dnd-kit/core": "^4.0.0",
|
||||
"@dnd-kit/sortable": "^5.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"@parcel/config-default": "^2.6.0",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@redux-devtools/extension": "^3.2.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@tanstack/react-table": "^8.5.11",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/sharp": "^0.28.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@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",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"airdcpp-apisocket": "2.4.5-beta.1",
|
||||
"axios": "^1.3.4",
|
||||
"axios-cache-interceptor": "^1.0.1",
|
||||
"axios-rate-limit": "^1.3.0",
|
||||
"axios-simple-cache-adapter": "^1.1.0",
|
||||
"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",
|
||||
"dayjs": "^1.10.6",
|
||||
"ellipsize": "^0.1.0",
|
||||
"ellipsize": "^0.5.1",
|
||||
"express": "^4.17.1",
|
||||
"fastest-validator": "^1.11.0",
|
||||
"filename-parser": "^1.0.2",
|
||||
"final-form": "^4.20.2",
|
||||
"final-form-arrays": "^3.0.2",
|
||||
"history": "^5.3.0",
|
||||
"html-to-text": "^8.1.0",
|
||||
"jsdoc": "^3.6.10",
|
||||
"lodash": "^4.17.21",
|
||||
"opds-extra": "^3.0.9",
|
||||
"node-sass": "npm:sass",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"qs": "^6.10.5",
|
||||
"react": "^18.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-collapsible": "^2.9.0",
|
||||
"react-comic-viewer": "^0.3.5",
|
||||
"react-day-picker": "^8.0.6",
|
||||
"react-comic-viewer": "^0.4.0",
|
||||
"react-day-picker": "^8.6.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-final-form": "^6.5.3",
|
||||
"react-final-form-arrays": "^3.1.3",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-final-form-arrays": "^3.1.4",
|
||||
"react-loader-spinner": "^4.0.0",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router": "^6.2.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router": "^6.9.0",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-select": "^5.3.2",
|
||||
"react-select-async-paginate": "^0.6.1",
|
||||
"react-select-async-paginate": "^0.7.2",
|
||||
"react-slick": "^0.29.0",
|
||||
"react-sliding-pane": "^7.1.0",
|
||||
"react-stickynode": "^4.0.0",
|
||||
"react-stickynode": "^4.1.0",
|
||||
"react-textarea-autosize": "^8.3.4",
|
||||
"reapop": "^4.0.5",
|
||||
"redux-first-history": "^5.0.9",
|
||||
"reapop": "^4.2.1",
|
||||
"redux-first-history": "^5.1.1",
|
||||
"redux-socket.io-middleware": "^1.0.4",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"sharp": "^0.30.5",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"socket.io-client": "^4.3.2",
|
||||
"styled-components": "^5.3.5",
|
||||
"styled-components": "^5.3.9",
|
||||
"threetwo-ui-typings": "^1.0.14",
|
||||
"voca": "^1.4.0",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"websocket": "^1.0.34",
|
||||
"ws": "^7.5.3",
|
||||
"xml2js": "^0.4.23",
|
||||
"xregexp": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -103,45 +89,30 @@
|
||||
"@babel/core": "^7.13.10",
|
||||
"@babel/plugin-syntax-top-level-await": "^7.14.5",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.13.10",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||
"@storybook/addon-actions": "^6.5.9",
|
||||
"@storybook/addon-essentials": "^6.5.9",
|
||||
"@storybook/addon-interactions": "^6.5.9",
|
||||
"@storybook/addon-links": "^6.5.9",
|
||||
"@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",
|
||||
"@storybook/addon-essentials": "^7.0.0-rc.3",
|
||||
"@storybook/addon-interactions": "^7.0.0-rc.3",
|
||||
"@storybook/addon-links": "^7.0.0-rc.3",
|
||||
"@storybook/blocks": "^7.0.0-rc.3",
|
||||
"@storybook/react": "^7.0.0-rc.3",
|
||||
"@storybook/react-vite": "^7.0.0-rc.3",
|
||||
"@storybook/testing-library": "^0.0.14-next.1",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@types/ellipsize": "^0.1.1",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^14.14.34",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"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",
|
||||
"buffer": "^6.0.3",
|
||||
"bulma": "^0.9.3",
|
||||
"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",
|
||||
"bulma": "^0.9.4",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
@@ -151,35 +122,19 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-storybook": "^0.6.11",
|
||||
"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",
|
||||
"jest": "^26.6.3",
|
||||
"mini-css-extract-plugin": "^1.4.1",
|
||||
"node-sass": "^7.0.1",
|
||||
"nodemon": "^1.17.3",
|
||||
"npm": "^8.11.0",
|
||||
"prettier": "^2.2.1",
|
||||
"react-refresh": "^0.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass-loader": "^11.0.1",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"string-similarity": "^4.0.4",
|
||||
"style-loader": "^2.0.0",
|
||||
"rimraf": "^4.1.3",
|
||||
"sass": "^1.58.1",
|
||||
"storybook": "^7.0.0-rc.3",
|
||||
"tslint": "^6.1.3",
|
||||
"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"
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
@@ -4,7 +4,10 @@ import {
|
||||
PriorityEnum,
|
||||
SearchResponse,
|
||||
} 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 {
|
||||
AIRDCPP_SEARCH_RESULTS_ADDED,
|
||||
AIRDCPP_SEARCH_RESULTS_UPDATED,
|
||||
@@ -18,6 +21,8 @@ import {
|
||||
IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
||||
AIRDCPP_TRANSFERS_FETCHED,
|
||||
LIBRARY_ISSUE_BUNDLES,
|
||||
AIRDCPP_SOCKET_CONNECTED,
|
||||
AIRDCPP_SOCKET_DISCONNECTED,
|
||||
} from "../constants/action-types";
|
||||
import { isNil } from "lodash";
|
||||
import axios from "axios";
|
||||
@@ -32,190 +37,199 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const toggleAirDCPPSocketConnectionStatus =
|
||||
(status: String, payload?: any) => async (dispatch) => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
dispatch({
|
||||
type: AIRDCPP_SOCKET_CONNECTED,
|
||||
data: payload,
|
||||
});
|
||||
break;
|
||||
|
||||
case "disconnected":
|
||||
dispatch({
|
||||
type: AIRDCPP_SOCKET_DISCONNECTED,
|
||||
data: payload,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Can't set AirDC++ socket status.");
|
||||
break;
|
||||
}
|
||||
};
|
||||
export const search =
|
||||
(data: SearchData, ADCPPSocket: any, credentials: any) =>
|
||||
async (dispatch) => {
|
||||
try {
|
||||
if (!ADCPPSocket.isConnected()) {
|
||||
await ADCPPSocket.connect(
|
||||
credentials.username,
|
||||
credentials.password,
|
||||
true,
|
||||
);
|
||||
}
|
||||
const instance: SearchInstance = await ADCPPSocket.post("search");
|
||||
dispatch({
|
||||
type: AIRDCPP_SEARCH_IN_PROGRESS,
|
||||
});
|
||||
|
||||
// We want to get notified about every new result in order to make the user experience better
|
||||
await ADCPPSocket.addListener(
|
||||
`search`,
|
||||
"search_result_added",
|
||||
async (groupedResult) => {
|
||||
// ...add the received result in the UI
|
||||
// (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results)
|
||||
|
||||
dispatch({
|
||||
type: AIRDCPP_SEARCH_RESULTS_ADDED,
|
||||
groupedResult,
|
||||
});
|
||||
},
|
||||
instance.id,
|
||||
);
|
||||
|
||||
// We also want to update the existing items in our list when new hits arrive for the previously listed files/directories
|
||||
await ADCPPSocket.addListener(
|
||||
`search`,
|
||||
"search_result_updated",
|
||||
async (groupedResult) => {
|
||||
// ...update properties of the existing result in the UI
|
||||
dispatch({
|
||||
type: AIRDCPP_SEARCH_RESULTS_UPDATED,
|
||||
groupedResult,
|
||||
});
|
||||
},
|
||||
instance.id,
|
||||
);
|
||||
|
||||
// We need to show something to the user in case the search won't yield any results so that he won't be waiting forever)
|
||||
// Wait for 5 seconds for any results to arrive after the searches were sent to the hubs
|
||||
await ADCPPSocket.addListener(
|
||||
`search`,
|
||||
"search_hub_searches_sent",
|
||||
async (searchInfo) => {
|
||||
await sleep(5000);
|
||||
|
||||
// Check the number of received results (in real use cases we should know that even without calling the API)
|
||||
const currentInstance = await ADCPPSocket.get(
|
||||
`search/${instance.id}`,
|
||||
);
|
||||
if (currentInstance.result_count === 0) {
|
||||
// ...nothing was received, show an informative message to the user
|
||||
console.log("No more search results.");
|
||||
}
|
||||
|
||||
// The search can now be considered to be "complete"
|
||||
// If there's an "in progress" indicator in the UI, that could also be disabled here
|
||||
dispatch({
|
||||
type: AIRDCPP_HUB_SEARCHES_SENT,
|
||||
searchInfo,
|
||||
instance,
|
||||
});
|
||||
},
|
||||
instance.id,
|
||||
);
|
||||
// Finally, perform the actual search
|
||||
await ADCPPSocket.post(`search/${instance.id}/hub_search`, data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
async (dispatch) => {
|
||||
try {
|
||||
if (!ADCPPSocket.isConnected()) {
|
||||
await ADCPPSocket();
|
||||
}
|
||||
};
|
||||
const instance: SearchInstance = await ADCPPSocket.post("search");
|
||||
dispatch({
|
||||
type: AIRDCPP_SEARCH_IN_PROGRESS,
|
||||
});
|
||||
|
||||
// We want to get notified about every new result in order to make the user experience better
|
||||
await ADCPPSocket.addListener(
|
||||
`search`,
|
||||
"search_result_added",
|
||||
async (groupedResult) => {
|
||||
// ...add the received result in the UI
|
||||
// (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results)
|
||||
|
||||
dispatch({
|
||||
type: AIRDCPP_SEARCH_RESULTS_ADDED,
|
||||
groupedResult,
|
||||
});
|
||||
},
|
||||
instance.id,
|
||||
);
|
||||
|
||||
// We also want to update the existing items in our list when new hits arrive for the previously listed files/directories
|
||||
await ADCPPSocket.addListener(
|
||||
`search`,
|
||||
"search_result_updated",
|
||||
async (groupedResult) => {
|
||||
// ...update properties of the existing result in the UI
|
||||
dispatch({
|
||||
type: AIRDCPP_SEARCH_RESULTS_UPDATED,
|
||||
groupedResult,
|
||||
});
|
||||
},
|
||||
instance.id,
|
||||
);
|
||||
|
||||
// We need to show something to the user in case the search won't yield any results so that he won't be waiting forever)
|
||||
// Wait for 5 seconds for any results to arrive after the searches were sent to the hubs
|
||||
await ADCPPSocket.addListener(
|
||||
`search`,
|
||||
"search_hub_searches_sent",
|
||||
async (searchInfo) => {
|
||||
await sleep(5000);
|
||||
|
||||
// Check the number of received results (in real use cases we should know that even without calling the API)
|
||||
const currentInstance = await ADCPPSocket.get(
|
||||
`search/${instance.id}`,
|
||||
);
|
||||
if (currentInstance.result_count === 0) {
|
||||
// ...nothing was received, show an informative message to the user
|
||||
console.log("No more search results.");
|
||||
}
|
||||
|
||||
// The search can now be considered to be "complete"
|
||||
// If there's an "in progress" indicator in the UI, that could also be disabled here
|
||||
dispatch({
|
||||
type: AIRDCPP_HUB_SEARCHES_SENT,
|
||||
searchInfo,
|
||||
instance,
|
||||
});
|
||||
},
|
||||
instance.id,
|
||||
);
|
||||
// Finally, perform the actual search
|
||||
await ADCPPSocket.post(`search/${instance.id}/hub_search`, data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadAirDCPPItem =
|
||||
(searchInstanceId: Number,
|
||||
(
|
||||
searchInstanceId: Number,
|
||||
resultId: String,
|
||||
comicObjectId: String,
|
||||
name: String, size: Number, type: any,
|
||||
name: String,
|
||||
size: Number,
|
||||
type: any,
|
||||
ADCPPSocket: any,
|
||||
credentials: any,
|
||||
): void =>
|
||||
async (dispatch) => {
|
||||
try {
|
||||
if (!ADCPPSocket.isConnected()) {
|
||||
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;
|
||||
async (dispatch) => {
|
||||
try {
|
||||
if (!ADCPPSocket.isConnected()) {
|
||||
await ADCPPSocket.connect();
|
||||
}
|
||||
};
|
||||
let bundleDBImportResult = {};
|
||||
const downloadResult = await ADCPPSocket.post(
|
||||
`search/${searchInstanceId}/results/${resultId}/download`,
|
||||
);
|
||||
|
||||
export const getBundlesForComic =
|
||||
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
|
||||
async (dispatch) => {
|
||||
try {
|
||||
if (!ADCPPSocket.isConnected()) {
|
||||
await ADCPPSocket.connect(
|
||||
`${credentials.username}`,
|
||||
`${credentials.password}`,
|
||||
true,
|
||||
);
|
||||
}
|
||||
const comicObject = await axios({
|
||||
if (!isNil(downloadResult)) {
|
||||
bundleDBImportResult = await axios({
|
||||
method: "POST",
|
||||
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
|
||||
url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
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) {
|
||||
const filteredBundles = comicObject.data.acquisition.directconnect.downloads.map(
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
return await ADCPPSocket.get(`queue/bundles/${bundleId}`);
|
||||
},
|
||||
);
|
||||
dispatch({
|
||||
type: AIRDCPP_BUNDLES_FETCHED,
|
||||
bundles: await Promise.all(filteredBundles),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
dispatch({
|
||||
type: AIRDCPP_BUNDLES_FETCHED,
|
||||
bundles: await Promise.all(filteredBundles),
|
||||
});
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getTransfers =
|
||||
(ADCPPSocket: any, credentials: any) => async (dispatch) => {
|
||||
try {
|
||||
if (!ADCPPSocket.isConnected()) {
|
||||
await ADCPPSocket.connect(
|
||||
`${credentials.username}`,
|
||||
`${credentials.password}`,
|
||||
true,
|
||||
);
|
||||
await ADCPPSocket.connect();
|
||||
}
|
||||
const bundles = await ADCPPSocket.get("queue/bundles/1/85", {});
|
||||
if (!isNil(bundles)) {
|
||||
@@ -234,7 +248,6 @@ export const getTransfers =
|
||||
type: LIBRARY_ISSUE_BUNDLES,
|
||||
issue_bundles,
|
||||
});
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import axios from "axios";
|
||||
import rateLimiter from "axios-rate-limit";
|
||||
import {
|
||||
AxiosCacheRequestConfig,
|
||||
createCacheAdapter,
|
||||
} from "axios-simple-cache-adapter";
|
||||
import qs from "qs";
|
||||
import { setupCache } from "axios-cache-interceptor";
|
||||
import {
|
||||
CV_SEARCH_SUCCESS,
|
||||
CV_API_CALL_IN_PROGRESS,
|
||||
@@ -26,25 +22,22 @@ import {
|
||||
LIBRARY_SERVICE_BASE_URI,
|
||||
} from "../constants/endpoints";
|
||||
|
||||
const axiosCacheAdapter = createCacheAdapter();
|
||||
const http = rateLimiter(axios.create(), {
|
||||
maxRequests: 1,
|
||||
perMilliseconds: 1000,
|
||||
maxRPS: 1,
|
||||
});
|
||||
|
||||
const cachedAxios = setupCache(axios);
|
||||
export const getWeeklyPullList = (options) => async (dispatch) => {
|
||||
try {
|
||||
dispatch({
|
||||
type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
|
||||
});
|
||||
|
||||
await axios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
|
||||
await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
|
||||
method: "get",
|
||||
params: options,
|
||||
axiosCacheAdapter,
|
||||
cache: 1000, // value in MS
|
||||
} as AxiosCacheRequestConfig).then((response) => {
|
||||
}).then((response) => {
|
||||
dispatch({
|
||||
type: CV_WEEKLY_PULLLIST_FETCHED,
|
||||
data: response.data.result,
|
||||
@@ -70,9 +63,6 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
paramsSerializer: (params) => {
|
||||
return qs.stringify(params, { arrayFormat: "repeat" });
|
||||
},
|
||||
});
|
||||
|
||||
switch (options.callURIAction) {
|
||||
|
||||
@@ -32,9 +32,11 @@ import {
|
||||
SS_SEARCH_FAILED,
|
||||
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||
WANTED_COMICS_FETCHED,
|
||||
VOLUMES_FETCHED,
|
||||
CV_WEEKLY_PULLLIST_FETCHED,
|
||||
} from "../constants/action-types";
|
||||
import { success } from "react-notification-system-redux";
|
||||
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
|
||||
|
||||
import { isNil, map } from "lodash";
|
||||
|
||||
export async function walkFolder(path: string): Promise<Array<IFolderData>> {
|
||||
@@ -58,15 +60,6 @@ export async function walkFolder(path: string): Promise<Array<IFolderData>> {
|
||||
* @return the comic book metadata
|
||||
*/
|
||||
export const fetchComicBookMetadata = () => async (dispatch) => {
|
||||
const extractionOptions = {
|
||||
extractTarget: "cover",
|
||||
targetExtractionFolder: "./userdata/covers",
|
||||
extractionMode: "bulk",
|
||||
paginationOptions: {
|
||||
pageLimit: 25,
|
||||
page: 1,
|
||||
},
|
||||
};
|
||||
dispatch({
|
||||
type: LS_IMPORT_CALL_IN_PROGRESS,
|
||||
});
|
||||
@@ -84,7 +77,7 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
|
||||
dispatch({
|
||||
type: LS_IMPORT,
|
||||
meta: { remote: true },
|
||||
data: { extractionOptions },
|
||||
data: {},
|
||||
});
|
||||
};
|
||||
export const toggleImportQueueStatus = (options) => async (dispatch) => {
|
||||
@@ -134,46 +127,50 @@ export const getComicBooks = (options) => async (dispatch) => {
|
||||
* @returns Nothing.
|
||||
* @param payload
|
||||
*/
|
||||
export const importToDB = (sourceName: string, payload?: any) => (dispatch) => {
|
||||
try {
|
||||
const comicBookMetadata = {
|
||||
rawFileDetails: {
|
||||
name: "",
|
||||
},
|
||||
importStatus: {
|
||||
isImported: true,
|
||||
tagged: false,
|
||||
matchedResult: {
|
||||
score: "0",
|
||||
export const importToDB =
|
||||
(sourceName: string, metadata?: any) => (dispatch) => {
|
||||
try {
|
||||
const comicBookMetadata = {
|
||||
importType: "new",
|
||||
payload: {
|
||||
rawFileDetails: {
|
||||
name: "",
|
||||
},
|
||||
importStatus: {
|
||||
isImported: true,
|
||||
tagged: false,
|
||||
matchedResult: {
|
||||
score: "0",
|
||||
},
|
||||
},
|
||||
sourcedMetadata: metadata || null,
|
||||
acquisition: { source: { wanted: true, name: sourceName } },
|
||||
},
|
||||
},
|
||||
sourcedMetadata: payload || null,
|
||||
acquisition: { source: { wanted: true, name: sourceName } },
|
||||
};
|
||||
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,
|
||||
});
|
||||
};
|
||||
dispatch({
|
||||
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: IMS_CV_METADATA_IMPORT_FAILED,
|
||||
importError: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
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) {
|
||||
dispatch({
|
||||
type: IMS_CV_METADATA_IMPORT_FAILED,
|
||||
importError: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchVolumeGroups = () => async (dispatch) => {
|
||||
try {
|
||||
@@ -194,6 +191,7 @@ export const fetchVolumeGroups = () => async (dispatch) => {
|
||||
};
|
||||
export const fetchComicVineMatches =
|
||||
(searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => {
|
||||
console.log(issueSearchQuery);
|
||||
try {
|
||||
dispatch({
|
||||
type: CV_API_CALL_IN_PROGRESS,
|
||||
@@ -223,7 +221,6 @@ export const fetchComicVineMatches =
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
let matches: any = [];
|
||||
if (
|
||||
!isNil(response.data.results) &&
|
||||
@@ -233,7 +230,6 @@ export const fetchComicVineMatches =
|
||||
} else {
|
||||
matches = response.data.map((match) => match);
|
||||
}
|
||||
console.log(matches);
|
||||
dispatch({
|
||||
type: CV_SEARCH_SUCCESS,
|
||||
searchResults: matches,
|
||||
@@ -252,33 +248,37 @@ export const fetchComicVineMatches =
|
||||
});
|
||||
};
|
||||
|
||||
export const extractComicArchive = (path: string) => async (dispatch) => {
|
||||
const comicBookPages: string[] = [];
|
||||
dispatch({
|
||||
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
||||
});
|
||||
const extractedComicBookArchive = await axios({
|
||||
method: "POST",
|
||||
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
data: {
|
||||
filePath: path,
|
||||
},
|
||||
});
|
||||
map(extractedComicBookArchive.data, (page) => {
|
||||
console.log(page);
|
||||
const pageFilePath = removeLeadingPeriod(page);
|
||||
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
|
||||
comicBookPages.push(imagePath);
|
||||
});
|
||||
dispatch({
|
||||
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
|
||||
extractedComicBookArchive: comicBookPages,
|
||||
});
|
||||
};
|
||||
/**
|
||||
* This method is a proxy to `uncompressFullArchive` which uncompresses complete `rar` or `zip` archives
|
||||
* @param {string} path The path to the compressed archive
|
||||
* @param {any} options Options object
|
||||
* @returns {any}
|
||||
*/
|
||||
export const extractComicArchive =
|
||||
(path: string, options: any): any =>
|
||||
async (dispatch) => {
|
||||
dispatch({
|
||||
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
||||
});
|
||||
await axios({
|
||||
method: "POST",
|
||||
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
data: {
|
||||
filePath: path,
|
||||
options,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Description
|
||||
* @param {any} query
|
||||
* @param {any} options
|
||||
* @returns {any}
|
||||
*/
|
||||
export const searchIssue = (query, options) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: SS_SEARCH_IN_PROGRESS,
|
||||
@@ -301,28 +301,32 @@ export const searchIssue = (query, options) => async (dispatch) => {
|
||||
case "wantedComicsPage":
|
||||
dispatch({
|
||||
type: WANTED_COMICS_FETCHED,
|
||||
data: response.data.body,
|
||||
data: response.data.hits,
|
||||
});
|
||||
break;
|
||||
case "globalSearchBar":
|
||||
dispatch({
|
||||
type: SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||
data: response.data.body,
|
||||
data: response.data.hits,
|
||||
});
|
||||
break;
|
||||
|
||||
case "libraryPage":
|
||||
dispatch({
|
||||
type: SS_SEARCH_RESULTS_FETCHED,
|
||||
data: response.data.body,
|
||||
data: response.data.hits,
|
||||
});
|
||||
break;
|
||||
case "volumesPage":
|
||||
dispatch({
|
||||
type: VOLUMES_FETCHED,
|
||||
data: response.data.hits,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
export const analyzeImage =
|
||||
(imageFilePath: string | Buffer) => async (dispatch) => {
|
||||
|
||||
@@ -16,7 +16,6 @@ export const saveSettings =
|
||||
method: "POST",
|
||||
data: { settingsPayload, settingsObjectId },
|
||||
});
|
||||
console.log(result.data);
|
||||
dispatch({
|
||||
type: SETTINGS_OBJECT_FETCHED,
|
||||
data: result.data,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@import "../../../../node_modules/bulma/bulma.sass";
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
@import "/node_modules/bulma/bulma.sass";
|
||||
$fa-font-path: "/node_modules/@fortawesome/fontawesome-free/webfonts";
|
||||
@import "/node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
@import "/node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
|
||||
@import "/node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
$bg-color: yellow;
|
||||
$border-color: red;
|
||||
|
||||
@@ -62,6 +62,10 @@ pre {
|
||||
margin-left: -300px;
|
||||
min-width: 500px;
|
||||
}
|
||||
.airdcpp-status {
|
||||
min-width: 300px;
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
body {
|
||||
background: #454a59;
|
||||
}
|
||||
@@ -356,6 +360,10 @@ pre {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.comic-viewer {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
// comicvine metadata
|
||||
.comicvine-metadata {
|
||||
background-color: #f2f1f9;
|
||||
@@ -410,20 +418,34 @@ pre {
|
||||
}
|
||||
}
|
||||
// Library
|
||||
.sticky {
|
||||
.header-area {
|
||||
width: 100%;
|
||||
padding: 25px 0 15px 0;
|
||||
position: sticky;
|
||||
top: 57px;
|
||||
z-index: 2;
|
||||
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: 146px;
|
||||
top: 250px;
|
||||
z-index: 1;
|
||||
background: #fffffc;
|
||||
min-height: 130px;
|
||||
|
||||
@@ -4,11 +4,25 @@ import { useDispatch } from "react-redux";
|
||||
import { saveSettings, deleteSettings } from "../../actions/settings.actions";
|
||||
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
|
||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
||||
import { isUndefined, isEmpty } from "lodash";
|
||||
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);
|
||||
@@ -25,7 +39,7 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
||||
airDCPPSettings.setSettings({});
|
||||
dispatch(deleteSettings());
|
||||
}, []);
|
||||
const validate = async () => {};
|
||||
const validate = async () => { };
|
||||
const initFormData = !isUndefined(
|
||||
airDCPPSettings.airDCPPState.settings.directConnect,
|
||||
)
|
||||
@@ -52,12 +66,24 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
||||
</Field>
|
||||
</span>
|
||||
</p>
|
||||
<p className="control is-expanded">
|
||||
<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++ host IP / hostname"
|
||||
placeholder="AirDC++ port"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,11 @@ import Dashboard from "./Dashboard/Dashboard";
|
||||
|
||||
import Import from "./Import";
|
||||
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
|
||||
import LibraryContainer from "./Library/LibraryContainer";
|
||||
import TabulatedContentContainer from "./Library/TabulatedContentContainer";
|
||||
import LibraryGrid from "./Library/LibraryGrid";
|
||||
import Search from "./Search";
|
||||
import Settings from "./Settings";
|
||||
import VolumeDetail from "./VolumeDetail/VolumeDetail";
|
||||
import PullList from "./PullList/PullList";
|
||||
import WantedComics from "./WantedComics/WantedComics";
|
||||
import Volumes from "./Volumes/Volumes";
|
||||
import Downloads from "./Downloads/Downloads";
|
||||
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
@@ -21,17 +18,24 @@ import {
|
||||
AirDCPPSocketContext,
|
||||
} from "../context/AirDCPPSocket";
|
||||
import { isEmpty, isUndefined } from "lodash";
|
||||
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types";
|
||||
import { useDispatch } from "react-redux";
|
||||
import axios from "axios";
|
||||
import { LIBRARY_SERVICE_BASE_URI } from "../constants/endpoints";
|
||||
import { useParams } from "react-router";
|
||||
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 foo = async () => {
|
||||
const initializeAirDCPPEventListeners = async () => {
|
||||
if (
|
||||
!isUndefined(airDCPPConfiguration.airDCPPState) &&
|
||||
!isEmpty(airDCPPConfiguration.airDCPPState.settings) &&
|
||||
@@ -42,9 +46,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
|
||||
"queue_bundle_added",
|
||||
async (data) => {
|
||||
console.log("JEMEN:", data);
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
);
|
||||
// download tick listener
|
||||
await airDCPPConfiguration.airDCPPState.socket.addListener(
|
||||
@@ -62,9 +64,18 @@ const AirDCPPSocketComponent = (): ReactElement => {
|
||||
`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
|
||||
console.log("IM THE MAN UP IN THIS")
|
||||
if (count < 1) {
|
||||
console.log(`[AirDCPP]: Download complete.`);
|
||||
dispatch({
|
||||
type: LS_SINGLE_IMPORT,
|
||||
meta: { remote: true },
|
||||
data: bundleData,
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -79,7 +90,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
|
||||
);
|
||||
}
|
||||
};
|
||||
foo();
|
||||
initializeAirDCPPEventListeners();
|
||||
}, [airDCPPConfiguration]);
|
||||
return <></>;
|
||||
};
|
||||
@@ -92,7 +103,10 @@ export const App = (): ReactElement => {
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/import" element={<Import path={"./comics"} />} />
|
||||
<Route path="/library" element={<LibraryContainer />} />
|
||||
<Route
|
||||
path="/library"
|
||||
element={<TabulatedContentContainer category="library" />}
|
||||
/>
|
||||
<Route path="/library-grid" element={<LibraryGrid />} />
|
||||
<Route path="/downloads" element={<Downloads data={{}} />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
@@ -105,9 +119,18 @@ export const App = (): ReactElement => {
|
||||
element={<VolumeDetail />}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/pull-list/all" element={<PullList />} />
|
||||
<Route path="/wanted/all" element={<WantedComics />} />
|
||||
<Route path="/volumes/all" element={<Volumes />} />
|
||||
<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>
|
||||
|
||||
@@ -16,6 +16,7 @@ import ellipsize from "ellipsize";
|
||||
import { Form, Field } from "react-final-form";
|
||||
import { isEmpty, isNil, map } from "lodash";
|
||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
||||
|
||||
interface IAcquisitionPanelProps {
|
||||
query: any;
|
||||
comicObjectId: any;
|
||||
@@ -96,9 +97,12 @@ export const AcquisitionPanel = (
|
||||
(searchInstanceId, resultId, name, size, type) => {
|
||||
dispatch(
|
||||
downloadAirDCPPItem(
|
||||
searchInstanceId, resultId,
|
||||
searchInstanceId,
|
||||
resultId,
|
||||
props.comicObjectId,
|
||||
name, size, type,
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
airDCPPConfiguration.airDCPPState.socket,
|
||||
{
|
||||
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
|
||||
@@ -163,7 +167,7 @@ export const AcquisitionPanel = (
|
||||
}
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<img src="/img/airdcpp_logo.svg" />
|
||||
<img src="/src/client/assets/img/airdcpp_logo.svg" />
|
||||
</span>
|
||||
<span className="airdcpp-text">Search on AirDC++</span>
|
||||
</button>
|
||||
@@ -332,4 +336,4 @@ export const AcquisitionPanel = (
|
||||
);
|
||||
};
|
||||
|
||||
export default AcquisitionPanel;
|
||||
export default AcquisitionPanel;
|
||||
@@ -18,7 +18,6 @@ export const Menu = (props): ReactElement => {
|
||||
} else if (!isEmpty(data.sourcedMetadata)) {
|
||||
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
|
||||
}
|
||||
|
||||
dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery));
|
||||
setSlidingPanelContentId("CVMatches");
|
||||
setVisible(true);
|
||||
|
||||
@@ -5,6 +5,7 @@ import Card from "../Carda";
|
||||
import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
|
||||
|
||||
import { RawFileDetails } from "./RawFileDetails";
|
||||
import { ComicVineSearchForm } from "../ComicVineSearchForm";
|
||||
|
||||
import TabControls from "./TabControls";
|
||||
import { EditMetadataPanel } from "./EditMetadataPanel";
|
||||
@@ -40,6 +41,15 @@ type ComicDetailProps = {};
|
||||
*/
|
||||
|
||||
export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
const {
|
||||
data: {
|
||||
_id,
|
||||
rawFileDetails,
|
||||
inferredMetadata,
|
||||
sourcedMetadata: { comicvine, locg, comicInfo },
|
||||
},
|
||||
userSettings,
|
||||
} = data;
|
||||
const [page, setPage] = useState(1);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
|
||||
@@ -56,7 +66,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
);
|
||||
|
||||
const extractedComicBook = useSelector(
|
||||
(state: RootState) => state.fileOps.extractedComicBookArchive,
|
||||
(state: RootState) => state.fileOps.extractedComicBookArchive.reading,
|
||||
);
|
||||
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
||||
|
||||
@@ -64,12 +74,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
|
||||
const openModal = useCallback((filePath) => {
|
||||
setIsOpen(true);
|
||||
dispatch(extractComicArchive(filePath));
|
||||
dispatch(
|
||||
extractComicArchive(filePath, {
|
||||
type: "full",
|
||||
purpose: "reading",
|
||||
imageResizeOptions: {
|
||||
baseWidth: 1024,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const afterOpenModal = useCallback(() => {
|
||||
const afterOpenModal = useCallback((things) => {
|
||||
// references are now sync'd and can be accessed.
|
||||
// subtitle.style.color = "#f00";
|
||||
console.log("kolaveri", things);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
@@ -79,9 +98,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
// sliding panel init
|
||||
const contentForSlidingPanel = {
|
||||
CVMatches: {
|
||||
content: () => {
|
||||
if (!comicVineAPICallProgress) {
|
||||
return (
|
||||
content: (props) => (
|
||||
<>
|
||||
<div className="card search-criteria-card">
|
||||
<div className="card-content">
|
||||
<ComicVineSearchForm data={rawFileDetails} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="is-size-5 mt-3 mb-2 ml-3">Searching for:</p>
|
||||
{inferredMetadata.issue ? (
|
||||
<div className="ml-3">
|
||||
<span className="tag mr-3">{inferredMetadata.issue.name} </span>
|
||||
<span className="tag"> # {inferredMetadata.issue.number} </span>
|
||||
</div>
|
||||
) : null}
|
||||
{!comicVineAPICallProgress ? (
|
||||
<ComicVineMatchPanel
|
||||
props={{
|
||||
comicVineSearchQueryObject,
|
||||
@@ -90,9 +121,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
comicObjectId,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
) : (
|
||||
<div className="progress-indicator-container">
|
||||
<div className="indicator">
|
||||
<Loader
|
||||
@@ -105,23 +134,15 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
editComicBookMetadata: {
|
||||
content: () => <EditMetadataPanel />,
|
||||
},
|
||||
};
|
||||
const {
|
||||
data: {
|
||||
_id,
|
||||
rawFileDetails,
|
||||
inferredMetadata,
|
||||
sourcedMetadata: { comicvine, locg, comicInfo },
|
||||
},
|
||||
userSettings,
|
||||
} = data;
|
||||
|
||||
// check for the availability of CV metadata
|
||||
const isComicBookMetadataAvailable =
|
||||
@@ -238,7 +259,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
{/* raw file details */}
|
||||
<div className="column is-three-fifths">
|
||||
<div className="column">
|
||||
{!isUndefined(rawFileDetails) &&
|
||||
!isEmpty(rawFileDetails.cover) && (
|
||||
<>
|
||||
@@ -269,6 +290,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
<ComicViewer
|
||||
pages={extractedComicBook}
|
||||
direction="ltr"
|
||||
className={{closeButton: "border: 1px solid red;"}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
@@ -295,4 +317,4 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ComicDetail;
|
||||
export default ComicDetail;
|
||||
@@ -7,7 +7,7 @@ import Card from "../Carda";
|
||||
export const ComicVineDetails = (props): ReactElement => {
|
||||
const { data, updatedAt } = props;
|
||||
return (
|
||||
<div className="column is-two-thirds">
|
||||
<div className="column is-half">
|
||||
<div className="comic-detail comicvine-metadata">
|
||||
<dl>
|
||||
<dt>ComicVine Metadata</dt>
|
||||
|
||||
@@ -10,38 +10,8 @@ export const ComicVineMatchPanel = (comicVineData): ReactElement => {
|
||||
comicVineAPICallProgress,
|
||||
comicVineSearchResults,
|
||||
} = comicVineData.props;
|
||||
console.log(comicVineData);
|
||||
return (
|
||||
<>
|
||||
{!isEmpty(comicVineSearchQueryObject) && (
|
||||
<div className="card search-criteria-card">
|
||||
<div className="card-content">
|
||||
<ComicVineSearchForm />
|
||||
<p className="is-size-6">Searching against:</p>
|
||||
<div className="field is-grouped is-grouped-multiline">
|
||||
<div className="control">
|
||||
<div className="tags has-addons">
|
||||
<span className="tag">Title</span>
|
||||
<span className="tag is-info">
|
||||
{comicVineSearchQueryObject.issue.inferredIssueDetails.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div className="tags has-addons">
|
||||
<span className="tag">Number</span>
|
||||
<span className="tag is-info">
|
||||
{
|
||||
comicVineSearchQueryObject.issue.inferredIssueDetails
|
||||
.number
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="search-results-container">
|
||||
{!isEmpty(comicVineSearchResults) && (
|
||||
<MatchResult
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { useEffect, useContext, ReactElement } from "react";
|
||||
import {
|
||||
getBundlesForComic,
|
||||
} from "../../actions/airdcpp.actions";
|
||||
import { getBundlesForComic } from "../../actions/airdcpp.actions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "threetwo-ui-typings";
|
||||
import { isEmpty, isNil, map } from "lodash";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { isUndefined } from "lodash";
|
||||
import { isEmpty } from "lodash";
|
||||
|
||||
export const RawFileDetails = (props): ReactElement => {
|
||||
const { rawFileDetails, inferredMetadata } = props.data;
|
||||
return (
|
||||
<>
|
||||
<div className="comic-detail raw-file-details">
|
||||
<div className="comic-detail raw-file-details column is-three-fifths">
|
||||
<dl>
|
||||
<dt>Raw File Details</dt>
|
||||
<dd className="is-size-7">
|
||||
@@ -34,11 +34,20 @@ export const RawFileDetails = (props): ReactElement => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div className="tags has-addons">
|
||||
<span className="tag">MIME type</span>
|
||||
<span className="tag is-primary is-light">
|
||||
{rawFileDetails.mimeType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div className="content comic-detail raw-file-details mt-3">
|
||||
|
||||
<div className="content comic-detail raw-file-details mt-3 column is-three-fifths">
|
||||
<dl>
|
||||
{/* inferred metadata */}
|
||||
<dt>Inferred Issue Metadata</dt>
|
||||
@@ -52,7 +61,7 @@ export const RawFileDetails = (props): ReactElement => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isUndefined(inferredMetadata.issue.number) ? (
|
||||
{!isEmpty(inferredMetadata.issue.number) ? (
|
||||
<div className="control">
|
||||
<div className="tags has-addons">
|
||||
<span className="tag">Number</span>
|
||||
@@ -80,6 +89,7 @@ RawFileDetails.propTypes = {
|
||||
fileSize: PropTypes.number,
|
||||
path: PropTypes.string,
|
||||
extension: PropTypes.string,
|
||||
mimeType: PropTypes.string,
|
||||
cover: PropTypes.shape({
|
||||
filePath: PropTypes.string,
|
||||
}),
|
||||
@@ -93,4 +103,4 @@ RawFileDetails.propTypes = {
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -14,7 +14,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
(state: RootState) => state.fileOps.comicBookExtractionInProgress,
|
||||
);
|
||||
const extractedComicBookArchive = useSelector(
|
||||
(state: RootState) => state.fileOps.extractedComicBookArchive,
|
||||
(state: RootState) => state.fileOps.extractedComicBookArchive.analysis,
|
||||
);
|
||||
|
||||
const imageAnalysisResult = useSelector((state: RootState) => {
|
||||
@@ -23,7 +23,15 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const unpackComicArchive = useCallback(() => {
|
||||
dispatch(extractComicArchive(data.rawFileDetails.filePath));
|
||||
dispatch(
|
||||
extractComicArchive(data.rawFileDetails.filePath, {
|
||||
type: "full",
|
||||
purpose: "analysis",
|
||||
imageResizeOptions: {
|
||||
baseWidth: 275,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// sliding panel config
|
||||
@@ -118,4 +126,4 @@ export const ArchiveOperations = (props): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ArchiveOperations;
|
||||
export default ArchiveOperations;
|
||||
@@ -1,22 +1,31 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { Form, Field } from "react-final-form";
|
||||
import Collapsible from "react-collapsible";
|
||||
import { fetchComicVineMatches } from "../actions/fileops.actions";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
/**
|
||||
* Component for accepting ComicVine search parameters
|
||||
* Component for performing search against ComicVine
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* const age = 21
|
||||
* const name = 'Jitendra Nirnejak'
|
||||
* return (
|
||||
* <User age={age} name={name} />
|
||||
* <ComicVineSearchForm data={rawFileDetails} />
|
||||
* )
|
||||
*/
|
||||
export const ComicVineSearchForm = () => {
|
||||
const onSubmit = () => {
|
||||
return true;
|
||||
};
|
||||
export const ComicVineSearchForm = (data) => {
|
||||
const dispatch = useDispatch();
|
||||
const onSubmit = useCallback((value) => {
|
||||
const userInititatedQuery = {
|
||||
inferredIssueDetails: {
|
||||
name: value.issueName,
|
||||
number: value.issueNumber,
|
||||
subtitle: "",
|
||||
year: value.issueYear,
|
||||
},
|
||||
};
|
||||
dispatch(fetchComicVineMatches(data, userInititatedQuery));
|
||||
}, []);
|
||||
const validate = () => {
|
||||
return true;
|
||||
};
|
||||
@@ -28,7 +37,7 @@ export const ComicVineSearchForm = () => {
|
||||
render={({ handleSubmit }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<span className="field is-normal">
|
||||
<label className="label">Issue Details</label>
|
||||
<label className="label mb-2 is-size-5">Search Manually</label>
|
||||
</span>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-body">
|
||||
@@ -48,11 +57,15 @@ export const ComicVineSearchForm = () => {
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<Field name="issueNumber">
|
||||
{(props) => (
|
||||
<p className="control is-expanded has-icons-left">
|
||||
<p className="control has-icons-left">
|
||||
<input
|
||||
{...props.input}
|
||||
className="input is-normal"
|
||||
@@ -65,18 +78,36 @@ export const ComicVineSearchForm = () => {
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<Field name="issueYear">
|
||||
{(props) => (
|
||||
<p className="control has-icons-left">
|
||||
<input
|
||||
{...props.input}
|
||||
className="input is-normal"
|
||||
placeholder="Type the issue year"
|
||||
/>
|
||||
<span className="icon is-small is-left">
|
||||
<i className="fas fa-hashtag"></i>
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<button
|
||||
type="submit"
|
||||
className="button is-info is-light is-outlined is-small"
|
||||
className="button is-success is-light is-outlined is-small"
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-hand-sparkles"></i>
|
||||
<i className="fas fa-search"></i>
|
||||
</span>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
@@ -89,16 +120,7 @@ export const ComicVineSearchForm = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
trigger={"Match Manually"}
|
||||
triggerTagName="a"
|
||||
triggerClassName={"is-size-6"}
|
||||
triggerOpenedClassName={"is-size-6"}
|
||||
>
|
||||
<MyForm />
|
||||
</Collapsible>
|
||||
);
|
||||
return <MyForm />;
|
||||
};
|
||||
|
||||
export default ComicVineSearchForm;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
getComicBooks,
|
||||
} from "../../actions/fileops.actions";
|
||||
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
||||
import { isEmpty } from "lodash";
|
||||
import { isEmpty, isNil } from "lodash";
|
||||
|
||||
export const Dashboard = (): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -43,7 +43,7 @@ export const Dashboard = (): ReactElement => {
|
||||
}, []);
|
||||
|
||||
const recentComics = useSelector(
|
||||
(state: RootState) => state.fileOps.recentComics,
|
||||
(state: RootState) => state.fileOps.recentComics
|
||||
);
|
||||
const wantedComics = useSelector(
|
||||
(state: RootState) => state.fileOps.wantedComics,
|
||||
@@ -60,7 +60,7 @@ export const Dashboard = (): ReactElement => {
|
||||
<section className="section">
|
||||
<h1 className="title">Dashboard</h1>
|
||||
|
||||
{!isEmpty(recentComics) && !isEmpty(recentComics.docs) ? (
|
||||
{!isEmpty(recentComics) ? (
|
||||
<>
|
||||
{/* Pull List */}
|
||||
<PullList issues={recentComics} />
|
||||
@@ -74,9 +74,8 @@ export const Dashboard = (): ReactElement => {
|
||||
<WantedComicsList comics={wantedComics} />
|
||||
)}
|
||||
{/* Recent imports */}
|
||||
{!isEmpty(recentComics) && (
|
||||
<RecentlyImported comicBookCovers={recentComics} />
|
||||
)}
|
||||
|
||||
{/* Volumes */}
|
||||
{!isEmpty(volumeGroups) && (
|
||||
<VolumeGroups volumeGroups={volumeGroups} />
|
||||
@@ -95,4 +94,4 @@ export const Dashboard = (): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
@@ -20,7 +20,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getWeeklyPullList({
|
||||
startDate: "2022-8-9",
|
||||
startDate: "2023-5-25",
|
||||
pageSize: "15",
|
||||
currentPage: "1",
|
||||
}),
|
||||
@@ -127,7 +127,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
<Slider {...settings} ref={(c) => (sliderRef = c)}>
|
||||
{!isNil(pullList) &&
|
||||
pullList &&
|
||||
map(pullList, (issue, idx) => {
|
||||
map(pullList, ({issue}, idx) => {
|
||||
return (
|
||||
<Card
|
||||
key={idx}
|
||||
@@ -161,4 +161,4 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PullList;
|
||||
export default PullList;
|
||||
@@ -2,7 +2,6 @@ import React, { ReactElement } from "react";
|
||||
import Card from "../Carda";
|
||||
import { Link } from "react-router-dom";
|
||||
import ellipsize from "ellipsize";
|
||||
import { escapePoundSymbol } from "../../shared/utils/formatting.utils";
|
||||
import { isEmpty, isNil, isUndefined, map } from "lodash";
|
||||
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
||||
import Masonry from "react-masonry-css";
|
||||
@@ -24,7 +23,6 @@ export const RecentlyImported = ({
|
||||
700: 2,
|
||||
600: 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="content mt-5">
|
||||
@@ -41,7 +39,7 @@ export const RecentlyImported = ({
|
||||
columnClassName="recent-comics-column"
|
||||
>
|
||||
{map(
|
||||
comicBookCovers.docs,
|
||||
comicBookCovers,
|
||||
(
|
||||
{
|
||||
_id,
|
||||
@@ -64,7 +62,6 @@ export const RecentlyImported = ({
|
||||
comicInfo,
|
||||
locg,
|
||||
});
|
||||
|
||||
const isComicBookMetadataAvailable =
|
||||
!isUndefined(comicvine) &&
|
||||
!isUndefined(comicvine.volumeInformation);
|
||||
@@ -86,14 +83,14 @@ export const RecentlyImported = ({
|
||||
{/* Raw file presence */}
|
||||
{isNil(rawFileDetails) && (
|
||||
<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>
|
||||
)}
|
||||
{/* ComicInfo.xml presence */}
|
||||
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
||||
<span className="icon custom-icon is-small has-text-danger">
|
||||
<img
|
||||
src="/img/comicinfoxml.svg"
|
||||
src="/src/client/assets/img/comicinfoxml.svg"
|
||||
alt={"ComicInfo.xml file detected."}
|
||||
/>
|
||||
</span>
|
||||
@@ -102,8 +99,8 @@ export const RecentlyImported = ({
|
||||
{isComicBookMetadataAvailable && (
|
||||
<span className="icon custom-icon">
|
||||
<img
|
||||
src="/img/cvlogo.svg"
|
||||
alt={"ComicInfo.xml file detected."}
|
||||
src="/src/client/assets/img/cvlogo.svg"
|
||||
alt={"ComicVine metadata detected."}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
@@ -123,12 +120,12 @@ export const RecentlyImported = ({
|
||||
</div>
|
||||
</Card>
|
||||
{/* metadata card */}
|
||||
{!isNil(name) ? (
|
||||
{!isNil(name) && (
|
||||
<Card orientation="horizontal" hasDetails imageUrl={coverURL}>
|
||||
<dd className="is-size-9">
|
||||
<dl>
|
||||
<span className="icon custom-icon">
|
||||
<img src={`/img/${icon}`} />
|
||||
<img src={`/src/client/assets/img/${icon}`} />
|
||||
</span>
|
||||
</dl>
|
||||
<dl>
|
||||
@@ -138,7 +135,7 @@ export const RecentlyImported = ({
|
||||
</dl>
|
||||
</dd>
|
||||
</Card>
|
||||
) : null}
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
@@ -146,4 +143,4 @@ export const RecentlyImported = ({
|
||||
</Masonry>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -82,12 +82,12 @@ export const WantedComicsList = ({
|
||||
{/* comicVine metadata presence */}
|
||||
{isComicBookMetadataAvailable && (
|
||||
<span className="icon custom-icon">
|
||||
<img src="/img/cvlogo.svg" />
|
||||
<img src="/src/client/assets/img/cvlogo.svg" />
|
||||
</span>
|
||||
)}
|
||||
{!isEmpty(locg) && (
|
||||
<span className="icon custom-icon">
|
||||
<img src="/img/locglogo.svg" />
|
||||
<img src="/src/client/assets/img/locglogo.svg" />
|
||||
</span>
|
||||
)}
|
||||
{/* Issue type */}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getTransfers } from "../../actions/airdcpp.actions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
import { searchIssue } from "../../actions/fileops.actions";
|
||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||
import MetadataPanel from "../shared/MetadataPanel";
|
||||
|
||||
@@ -47,7 +46,6 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||
})
|
||||
setBundles(foo);
|
||||
}
|
||||
|
||||
}, [issueBundles])
|
||||
|
||||
return !isNil(bundles) ?
|
||||
@@ -56,9 +54,9 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||
<h1 className="title">Downloads</h1>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
{bundles.map(bundle => {
|
||||
{bundles.map((bundle, idx) => {
|
||||
console.log(bundle);
|
||||
return <>
|
||||
return <div key={idx}>
|
||||
<MetadataPanel
|
||||
data={bundle}
|
||||
imageStyle={{ maxWidth: 80 }}
|
||||
@@ -70,30 +68,33 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
|
||||
margin: "0 0 8px 0",
|
||||
}} />
|
||||
|
||||
<table className="table is-size-7">
|
||||
<table className="table is-size-7">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Type</th>
|
||||
<th>Bundle ID</th>
|
||||
</tr>
|
||||
|
||||
{bundle.acquisition.directconnect.downloads.map((bundle) => {
|
||||
return(<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bundle.acquisition.directconnect.downloads.map((bundle, idx) => {
|
||||
return (<tr key={idx}>
|
||||
<td>{bundle.name}</td>
|
||||
<td>{bundle.size}</td>
|
||||
<td>{bundle.type.str}</td>
|
||||
<td><span className="tag is-warning">{bundle.bundleId}</span></td>
|
||||
</tr>)
|
||||
})}
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */}
|
||||
</>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div> : <div>asd</div>;
|
||||
</div> : <div>There are no downloads.</div>;
|
||||
};
|
||||
|
||||
export default Downloads;
|
||||
|
||||
@@ -31,7 +31,7 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
|
||||
from: 0,
|
||||
},
|
||||
type: "volumeName",
|
||||
trigger: "globalSearchBar"
|
||||
trigger: "globalSearchBar",
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -51,7 +51,6 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
|
||||
<i className="fa-solid fa-magnifying-glass"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!isEmpty(searchResults) ? (
|
||||
<div
|
||||
className="columns box is-multiline"
|
||||
@@ -62,9 +61,10 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
|
||||
margin: "60px 0 0 350px",
|
||||
}}
|
||||
>
|
||||
{map(searchResults, (result) => (
|
||||
{map(searchResults, (result, idx) => (
|
||||
<MetadataPanel
|
||||
data={result}
|
||||
key={idx}
|
||||
imageStyle={{ maxWidth: 70 }}
|
||||
titleStyle={{ fontSize: "0.8rem" }}
|
||||
tagsStyle={{ fontSize: "0.7rem" }}
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
import React, { useMemo, ReactElement, useCallback } from "react";
|
||||
import React, { useMemo, ReactElement, useCallback, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import T2Table from "../shared/T2Table";
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
import MetadataPanel from "../shared/MetadataPanel";
|
||||
import SearchBar from "./SearchBar";
|
||||
import { useDispatch } from "react-redux";
|
||||
import T2Table from "../shared/T2Table";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { searchIssue } from "../../actions/fileops.actions";
|
||||
import ellipsize from "ellipsize";
|
||||
|
||||
interface IComicBookLibraryProps {
|
||||
data: {
|
||||
searchResults: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
const { searchResults } = data.data;
|
||||
/**
|
||||
* Component that tabulates the contents of the user's ThreeTwo Library.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* <Library />
|
||||
*/
|
||||
export const Library = (): ReactElement => {
|
||||
const searchResults = useSelector(
|
||||
(state: RootState) => state.fileOps.libraryComics,
|
||||
);
|
||||
const searchError = useSelector((state: RootState) => state.fileOps.librarySearchError);
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
searchIssue(
|
||||
{
|
||||
query: {},
|
||||
},
|
||||
{
|
||||
pagination: {
|
||||
size: 15,
|
||||
from: 0,
|
||||
},
|
||||
type: "all",
|
||||
trigger: "libraryPage",
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// programatically navigate to comic detail
|
||||
const navigate = useNavigate();
|
||||
@@ -65,7 +86,8 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
const WantedStatus = ({ value }) => {
|
||||
return !value ? <span className="tag is-info is-light">Wanted</span> : null;
|
||||
};
|
||||
const columns = [
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: "Comic Metadata",
|
||||
footer: 1,
|
||||
@@ -75,7 +97,7 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
id: "fileDetails",
|
||||
minWidth: 400,
|
||||
accessorKey: "_source",
|
||||
cell: info => {
|
||||
cell: (info) => {
|
||||
return <MetadataPanel data={info.getValue()} />;
|
||||
},
|
||||
},
|
||||
@@ -84,11 +106,11 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
accessorKey: "_source.sourcedMetadata.comicInfo",
|
||||
align: "center",
|
||||
minWidth: 250,
|
||||
cell: info =>
|
||||
cell: (info) =>
|
||||
!isEmpty(info.getValue()) ? (
|
||||
<ComicInfoXML data={info.getValue()} />
|
||||
) : (
|
||||
<span className="tag">No ComicInfo.xml</span>
|
||||
<span className="tag mt-5">No ComicInfo.xml</span>
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -98,13 +120,12 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
columns: [
|
||||
{
|
||||
header: "Publisher",
|
||||
accessorKey:
|
||||
"_source.sourcedMetadata.comicvine.volumeInformation",
|
||||
cell: info => {
|
||||
accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation",
|
||||
cell: (info) => {
|
||||
return (
|
||||
!isNil(info.getValue()) && (
|
||||
<h6 className="is-size-7 has-text-weight-bold">
|
||||
{ info.getValue().publisher.name }
|
||||
{info.getValue().publisher.name}
|
||||
</h6>
|
||||
)
|
||||
);
|
||||
@@ -113,21 +134,29 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
{
|
||||
header: "Something",
|
||||
accessorKey: "_source.acquisition.source.wanted",
|
||||
cell: info => {
|
||||
!isUndefined(info.getValue()) ?
|
||||
<WantedStatus value={info.getValue().toString()} /> : "Nothing";
|
||||
cell: (info) => {
|
||||
!isUndefined(info.getValue()) ? (
|
||||
<WantedStatus value={info.getValue().toString()} />
|
||||
) : (
|
||||
"Nothing"
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
// ImportStatus.propTypes = {
|
||||
// value: PropTypes.bool.isRequired,
|
||||
// };
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const goToNextPage = useCallback((pageIndex, pageSize) => {
|
||||
/**
|
||||
* Pagination control that fetches the next x (pageSize) items
|
||||
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
|
||||
* @param {number} pageIndex
|
||||
* @param {number} pageSize
|
||||
* @returns void
|
||||
*
|
||||
**/
|
||||
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
|
||||
dispatch(
|
||||
searchIssue(
|
||||
{
|
||||
@@ -139,17 +168,25 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
from: pageSize * pageIndex + 1,
|
||||
},
|
||||
type: "all",
|
||||
trigger: "libraryPage",
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const goToPreviousPage = useCallback((pageIndex, pageSize) => {
|
||||
/**
|
||||
* Pagination control that fetches the previous x (pageSize) items
|
||||
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
|
||||
* @param {number} pageIndex
|
||||
* @param {number} pageSize
|
||||
* @returns void
|
||||
**/
|
||||
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
|
||||
let from = 0;
|
||||
if (pageIndex === 2) {
|
||||
from = (pageIndex - 1) * pageSize + 2 - 27;
|
||||
from = (pageIndex - 1) * pageSize + 2 - 17;
|
||||
} else {
|
||||
from = (pageIndex - 1) * pageSize + 2 - 26;
|
||||
from = (pageIndex - 1) * pageSize + 2 - 16;
|
||||
}
|
||||
dispatch(
|
||||
searchIssue(
|
||||
@@ -162,30 +199,54 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
|
||||
from,
|
||||
},
|
||||
type: "all",
|
||||
trigger: "libraryPage",
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// ImportStatus.propTypes = {
|
||||
// value: PropTypes.bool.isRequired,
|
||||
// };
|
||||
return (
|
||||
<section className="container">
|
||||
<div className="section">
|
||||
<h1 className="title">Library</h1>
|
||||
{/* Search bar */}
|
||||
<SearchBar />
|
||||
{!isUndefined(searchResults) && (
|
||||
<div className="header-area">
|
||||
<h1 className="title">Library</h1>
|
||||
</div>
|
||||
{!isEmpty(searchResults) ? (
|
||||
<div>
|
||||
<div className="library">
|
||||
<T2Table
|
||||
rowData={searchResults.hits.hits}
|
||||
totalPages={searchResults.hits.total.value}
|
||||
totalPages={searchResults.total.value}
|
||||
columns={columns}
|
||||
paginationHandlers={{
|
||||
nextPage: goToNextPage,
|
||||
previousPage: goToPreviousPage,
|
||||
}}
|
||||
sourceData={searchResults?.hits}
|
||||
rowClickHandler={navigateToComicDetail}
|
||||
paginationHandlers={{
|
||||
nextPage,
|
||||
previousPage,
|
||||
}}
|
||||
/>
|
||||
{/* pagination controls */}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="columns">
|
||||
<div className="column is-two-thirds">
|
||||
<article className="message is-link">
|
||||
<div className="message-body">
|
||||
No comics were found in the library, Elasticsearch reports no
|
||||
indices. Try importing a few comics into the library and come
|
||||
back.
|
||||
</div>
|
||||
</article>
|
||||
<pre>
|
||||
{!isUndefined(searchError.data) &&
|
||||
JSON.stringify(
|
||||
searchError.data.meta.body.error.root_cause,
|
||||
null,
|
||||
4,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { isEmpty, isUndefined } from "lodash";
|
||||
import React, { ReactElement, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { searchIssue } from "../../actions/fileops.actions";
|
||||
import { Library } from "./Library";
|
||||
|
||||
const LibraryContainer = (): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
searchIssue(
|
||||
{
|
||||
query: {},
|
||||
},
|
||||
{
|
||||
pagination: {
|
||||
size: 25,
|
||||
from: 0,
|
||||
},
|
||||
type: "all",
|
||||
trigger: "libraryPage"
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const searchResults = useSelector(
|
||||
(state: RootState) => state.fileOps.libraryComics,
|
||||
);
|
||||
const searchError = useSelector(
|
||||
(state: RootState) => state.fileOps.librarySearchError,
|
||||
);
|
||||
|
||||
return !isEmpty(searchResults) ? (
|
||||
<Library data={{ searchResults }} />
|
||||
) : (
|
||||
<div className="container">
|
||||
<section className="section is-small">
|
||||
<div className="columns">
|
||||
<div className="column is-two-thirds">
|
||||
<article className="message is-link">
|
||||
<div className="message-body">
|
||||
No comics were found in the library, Elasticsearch reports no
|
||||
indices. Try importing a few comics into the library and come
|
||||
back.
|
||||
</div>
|
||||
</article>
|
||||
<pre>
|
||||
{!isUndefined(searchError.data) &&
|
||||
JSON.stringify(
|
||||
searchError.data.meta.body.error.root_cause,
|
||||
null,
|
||||
4,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryContainer;
|
||||
@@ -73,7 +73,7 @@ export const LibraryGrid = (libraryGridProps: ILibraryGridProps) => {
|
||||
<div className="content is-flex is-flex-direction-row">
|
||||
{!isEmpty(sourcedMetadata.comicvine) && (
|
||||
<span className="icon cv-icon is-small">
|
||||
<img src="/dist/img/cvlogo.svg" />
|
||||
<img src="/src/client/assets/img/cvlogo.svg" />
|
||||
</span>
|
||||
)}
|
||||
{isNil(rawFileDetails) && (
|
||||
|
||||
@@ -21,19 +21,20 @@ export const SearchBar = (): ReactElement => {
|
||||
from: 0,
|
||||
},
|
||||
type: "volumeName",
|
||||
trigger: "libraryPage",
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
return (
|
||||
<div className="box sticky">
|
||||
<div className="box">
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{}}
|
||||
render={({ handleSubmit, form, submitting, pristine, values }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="field is-grouped">
|
||||
<div className="control is-expanded search">
|
||||
<div className="control search is-expanded">
|
||||
<Field name="search">
|
||||
{({ input, meta }) => {
|
||||
return (
|
||||
@@ -55,26 +56,7 @@ export const SearchBar = (): ReactElement => {
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
{/* <div className="column one-fifth">
|
||||
<div className="field has-addons">
|
||||
<p className="control">
|
||||
<button className="button">
|
||||
<span className="icon is-small">
|
||||
<i className="fa-solid fa-list"></i>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p className="control">
|
||||
<button className="button">
|
||||
<Link to="/library-grid">
|
||||
<span className="icon is-small">
|
||||
<i className="fa-solid fa-image"></i>
|
||||
</span>
|
||||
</Link>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
66
src/client/components/Library/TabulatedContentContainer.tsx
Normal file
66
src/client/components/Library/TabulatedContentContainer.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import PullList from "../PullList/PullList";
|
||||
import { Volumes } from "../Volumes/Volumes";
|
||||
import WantedComics from "../WantedComics/WantedComics";
|
||||
import { Library } from "./Library";
|
||||
|
||||
interface ITabulatedContentContainerProps {
|
||||
category: string;
|
||||
}
|
||||
/**
|
||||
* Component to draw the contents of a category in a table.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* return (
|
||||
* <TabulatedContentContainer category={"library"} />
|
||||
* )
|
||||
*/
|
||||
|
||||
const TabulatedContentContainer = (
|
||||
props: ITabulatedContentContainerProps,
|
||||
): ReactElement => {
|
||||
const { category } = props;
|
||||
const renderTabulatedContent = () => {
|
||||
switch (category) {
|
||||
case "library":
|
||||
return <Library />;
|
||||
case "pullList":
|
||||
return <PullList />;
|
||||
case "wanted":
|
||||
return <WantedComics />;
|
||||
case "volumes":
|
||||
return <Volumes />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return renderTabulatedContent();
|
||||
// : (
|
||||
// <div className="container">
|
||||
// <section className="section is-small">
|
||||
// <div className="columns">
|
||||
// <div className="column is-two-thirds">
|
||||
// <article className="message is-link">
|
||||
// <div className="message-body">
|
||||
// No comics were found in the library, and Elasticsearch doesn't have any
|
||||
// indices. Try resetting the library from <code>Settings > Flush DB & Temporary Folders</code> and then import your library again.
|
||||
// </div>
|
||||
// </article>
|
||||
// <pre>
|
||||
// {!isUndefined(searchError.data) &&
|
||||
// JSON.stringify(
|
||||
// searchError.data.meta.body.error.root_cause,
|
||||
// null,
|
||||
// 4,
|
||||
// )}
|
||||
// </pre>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
// </div>
|
||||
// );
|
||||
};
|
||||
|
||||
export default TabulatedContentContainer;
|
||||
@@ -1,21 +1,31 @@
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { SearchBar } from "./GlobalSearchBar/SearchBar";
|
||||
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { isUndefined } from "lodash";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
|
||||
const Navbar: React.FunctionComponent = (props) => {
|
||||
const downloadProgressTick = useSelector(
|
||||
(state: RootState) => state.airdcpp.downloadProgressData,
|
||||
);
|
||||
|
||||
const airDCPPSocketConnectionStatus = useSelector(
|
||||
(state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
|
||||
);
|
||||
const airDCPPSessionInfo = useSelector(
|
||||
(state: RootState) => state.airdcpp.airDCPPSessionInfo,
|
||||
);
|
||||
const socketDisconnectionReason = useSelector(
|
||||
(state: RootState) => state.airdcpp.socketDisconnectionReason,
|
||||
);
|
||||
return (
|
||||
<nav className="navbar is-fixed-top">
|
||||
<div className="navbar-brand">
|
||||
<Link to="/" className="navbar-item">
|
||||
<img
|
||||
src="/img/threetwo.svg"
|
||||
src="/src/client/assets/img/threetwo.svg"
|
||||
alt="ThreeTwo! A comic book curator"
|
||||
width="112"
|
||||
height="28"
|
||||
@@ -72,18 +82,66 @@ const Navbar: React.FunctionComponent = (props) => {
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
<a className="navbar-link is-arrowless">
|
||||
<i className="fa-solid fa-download"></i>
|
||||
{downloadProgressTick && (
|
||||
<div className="pulsating-circle"></div>
|
||||
)}
|
||||
{downloadProgressTick && <div className="pulsating-circle"></div>}
|
||||
</a>
|
||||
{!isUndefined(downloadProgressTick) ? (
|
||||
<div className="navbar-dropdown download-progress-meter">
|
||||
<div className="navbar-dropdown is-right is-boxed">
|
||||
<a className="navbar-item">
|
||||
<DownloadProgressTick data={downloadProgressTick} />
|
||||
</a>
|
||||
</div>
|
||||
</a> </div>
|
||||
) : null}
|
||||
</div>
|
||||
{/* AirDC++ socket connection status */}
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
{airDCPPSocketConnectionStatus ? (
|
||||
<>
|
||||
<a className="navbar-link is-arrowless has-text-success">
|
||||
<i className="fa-solid fa-bolt"></i>
|
||||
</a>
|
||||
<div className="navbar-dropdown pr-2 pl-2 is-right airdcpp-status is-boxed">
|
||||
{/* AirDC++ Session Information */}
|
||||
|
||||
<p>
|
||||
Last login was{" "}
|
||||
<span className="tag">
|
||||
{format(
|
||||
fromUnixTime(airDCPPSessionInfo.user.last_login),
|
||||
"dd MMMM, yyyy",
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<hr className="navbar-divider" />
|
||||
<p>
|
||||
<span className="tag has-text-success">
|
||||
{airDCPPSessionInfo.user.username}
|
||||
</span>
|
||||
connected to{" "}
|
||||
<span className="tag has-text-success">
|
||||
{airDCPPSessionInfo.system_info.client_version}
|
||||
</span>{" "}
|
||||
with session ID{" "}
|
||||
<span className="tag has-text-success">
|
||||
{airDCPPSessionInfo.session_id}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* <pre>{JSON.stringify(airDCPPSessionInfo, null, 2)}</pre> */}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a className="navbar-link is-arrowless has-text-danger">
|
||||
<i className="fa-solid fa-bolt"></i>
|
||||
</a>
|
||||
<div className="navbar-dropdown pr-2 pl-2 is-right is-boxed">
|
||||
<pre>
|
||||
{JSON.stringify(socketDisconnectionReason, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="navbar-item has-dropdown is-hoverable is-mega">
|
||||
<div className="navbar-link flex">Blog</div>
|
||||
<div id="blogDropdown" className="navbar-dropdown">
|
||||
|
||||
@@ -4,18 +4,19 @@ import { getWeeklyPullList } from "../../actions/comicinfo.actions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Card from "../Carda";
|
||||
import ellipsize from "ellipsize";
|
||||
import { isNil } from "lodash";
|
||||
|
||||
export const PullList = (): ReactElement => {
|
||||
const pullListComics = useSelector(
|
||||
(state: RootState) => state.comicInfo.pullList,
|
||||
);
|
||||
console.log(pullListComics);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getWeeklyPullList({
|
||||
startDate: "2022-6-1",
|
||||
pageSize: "100",
|
||||
startDate: "2022-11-15",
|
||||
pageSize: "15",
|
||||
currentPage: "1",
|
||||
}),
|
||||
);
|
||||
@@ -25,24 +26,25 @@ export const PullList = (): ReactElement => {
|
||||
const columnData = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: "Comic Information",
|
||||
header: "Comic Information",
|
||||
columns: [
|
||||
{
|
||||
Header: "Details",
|
||||
header: "Details",
|
||||
id: "comicDetails",
|
||||
minWidth: 450,
|
||||
accessor: (row) => {
|
||||
console.log(row);
|
||||
accessorKey: "issue",
|
||||
cell: (row) => {
|
||||
const item = row.getValue();
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<div className="column is-three-quarters">
|
||||
<div className="comic-detail issue-metadata">
|
||||
<dl>
|
||||
<dd>
|
||||
<div className="columns mt-2">
|
||||
<div className="column is-3">
|
||||
<Card
|
||||
imageUrl={row.cover}
|
||||
imageUrl={item.cover}
|
||||
orientation={"vertical"}
|
||||
hasDetails={false}
|
||||
// cardContainerStyle={{ maxWidth: 200 }}
|
||||
@@ -52,18 +54,20 @@ export const PullList = (): ReactElement => {
|
||||
<dl>
|
||||
<dt>
|
||||
<h6 className="name has-text-weight-medium mb-1">
|
||||
{row.name}
|
||||
{item.name}
|
||||
</h6>
|
||||
</dt>
|
||||
<dd className="is-size-7">
|
||||
published by{" "}
|
||||
<span className="has-text-weight-semibold">
|
||||
{row.publisher}
|
||||
{item.publisher}
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dd className="is-size-7">
|
||||
<span>{ellipsize(row.description, 190)}</span>
|
||||
<span>
|
||||
{ellipsize(item.description, 190)}
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dd className="is-size-7 mt-2">
|
||||
@@ -71,10 +75,10 @@ export const PullList = (): ReactElement => {
|
||||
<div className="control">
|
||||
<span className="tags">
|
||||
<span className="tag is-success is-light has-text-weight-semibold">
|
||||
{row.price}
|
||||
{item.price}
|
||||
</span>
|
||||
<span className="tag is-success is-light">
|
||||
{row.pulls}
|
||||
{item.pulls}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -99,16 +103,24 @@ export const PullList = (): ReactElement => {
|
||||
return (
|
||||
<section className="container">
|
||||
<div className="section">
|
||||
<h1 className="title">Weekly Pull List</h1>
|
||||
<T2Table
|
||||
rowData={pullListComics}
|
||||
columns={columnData}
|
||||
totalPages={pullListComics.length}
|
||||
paginationHandlers={{
|
||||
nextPage: nextPageHandler,
|
||||
previousPage: previousPageHandler,
|
||||
}}
|
||||
/>
|
||||
<div className="header-area">
|
||||
<h1 className="title">Weekly Pull List</h1>
|
||||
</div>
|
||||
{!isNil(pullListComics) && (
|
||||
<div>
|
||||
<div className="library">
|
||||
<T2Table
|
||||
sourceData={pullListComics}
|
||||
columns={columnData}
|
||||
totalPages={pullListComics.length}
|
||||
paginationHandlers={{
|
||||
nextPage: nextPageHandler,
|
||||
previousPage: previousPageHandler,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useCallback, ReactElement } from "react";
|
||||
import React, { useCallback, ReactElement } from "react";
|
||||
import { isNil, isEmpty } from "lodash";
|
||||
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
|
||||
import { importToDB } from "../actions/fileops.actions";
|
||||
|
||||
@@ -2,16 +2,13 @@ import React, { ReactElement, useEffect, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { searchIssue } from "../../actions/fileops.actions";
|
||||
import Card from "../Carda";
|
||||
import SearchBar from "../Library/SearchBar";
|
||||
import T2Table from "../shared/T2Table";
|
||||
import ellipsize from "ellipsize";
|
||||
import { isUndefined } from "lodash";
|
||||
import { convert } from "html-to-text";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export const Volumes = (props): ReactElement => {
|
||||
const volumes = useSelector(
|
||||
(state: RootState) => state.fileOps.librarySearchResults,
|
||||
);
|
||||
const volumes = useSelector((state: RootState) => state.fileOps.volumes);
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
@@ -25,19 +22,20 @@ export const Volumes = (props): ReactElement => {
|
||||
from: 0,
|
||||
},
|
||||
type: "volumes",
|
||||
trigger: "volumesPage",
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
console.log(volumes);
|
||||
|
||||
const columnData = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: "Volume Details",
|
||||
header: "Volume Details",
|
||||
id: "volumeDetails",
|
||||
minWidth: 450,
|
||||
accessor: (row) => {
|
||||
accessorKey: "_source",
|
||||
cell: (row) => {
|
||||
const foo = row.getValue();
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
@@ -45,11 +43,11 @@ export const Volumes = (props): ReactElement => {
|
||||
<dl>
|
||||
<dd>
|
||||
<div className="columns mt-2">
|
||||
<div className="column is-3">
|
||||
<div className="">
|
||||
<Card
|
||||
imageUrl={
|
||||
row._source.sourcedMetadata.comicvine
|
||||
.volumeInformation.image.thumb_url
|
||||
foo.sourcedMetadata.comicvine.volumeInformation
|
||||
.image.thumb_url
|
||||
}
|
||||
orientation={"vertical"}
|
||||
hasDetails={false}
|
||||
@@ -61,7 +59,7 @@ export const Volumes = (props): ReactElement => {
|
||||
<dt>
|
||||
<h6 className="name has-text-weight-medium mb-1">
|
||||
{
|
||||
row._source.sourcedMetadata.comicvine
|
||||
foo.sourcedMetadata.comicvine
|
||||
.volumeInformation.name
|
||||
}
|
||||
</h6>
|
||||
@@ -70,7 +68,7 @@ export const Volumes = (props): ReactElement => {
|
||||
published by{" "}
|
||||
<span className="has-text-weight-semibold">
|
||||
{
|
||||
row._source.sourcedMetadata.comicvine
|
||||
foo.sourcedMetadata.comicvine
|
||||
.volumeInformation.publisher.name
|
||||
}
|
||||
</span>
|
||||
@@ -80,7 +78,7 @@ export const Volumes = (props): ReactElement => {
|
||||
<span>
|
||||
{ellipsize(
|
||||
convert(
|
||||
row._source.sourcedMetadata.comicvine
|
||||
foo.sourcedMetadata.comicvine
|
||||
.volumeInformation.description,
|
||||
{
|
||||
baseElements: {
|
||||
@@ -102,7 +100,7 @@ export const Volumes = (props): ReactElement => {
|
||||
</span>
|
||||
<span className="tag is-success is-light">
|
||||
{
|
||||
row._source.sourcedMetadata.comicvine
|
||||
foo.sourcedMetadata.comicvine
|
||||
.volumeInformation.count_of_issues
|
||||
}
|
||||
</span>
|
||||
@@ -122,36 +120,35 @@ export const Volumes = (props): ReactElement => {
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Download Status",
|
||||
header: "Download Status",
|
||||
columns: [
|
||||
{
|
||||
Header: "Files",
|
||||
accessor: "_source.acquisition.directconnect",
|
||||
header: "Files",
|
||||
accessorKey: "_source.acquisition.directconnect",
|
||||
align: "right",
|
||||
Cell: (props) => {
|
||||
cell: (props) => {
|
||||
const row = props.getValue();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
// flexDirection: "column",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{props.cell.value.length > 0 ? (
|
||||
<span className="tag is-warning">
|
||||
{props.cell.value.length}
|
||||
</span>
|
||||
{row.length > 0 ? (
|
||||
<span className="tag is-warning">{row.length}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Type",
|
||||
header: "Type",
|
||||
id: "Air",
|
||||
},
|
||||
{
|
||||
Header: "Type",
|
||||
header: "Type",
|
||||
id: "dcc",
|
||||
},
|
||||
],
|
||||
@@ -162,16 +159,24 @@ export const Volumes = (props): ReactElement => {
|
||||
return (
|
||||
<section className="container">
|
||||
<div className="section">
|
||||
{volumes.hits ? (
|
||||
<div className="header-area">
|
||||
<h1 className="title">Volumes</h1>
|
||||
</div>
|
||||
{!isUndefined(volumes.hits) && (
|
||||
<div>
|
||||
<div className="library">
|
||||
<h1 className="title">Volumes</h1>
|
||||
{/* Search bar */}
|
||||
<SearchBar />
|
||||
<T2Table rowData={volumes.hits.hits} columns={columnData} />
|
||||
<T2Table
|
||||
sourceData={volumes?.hits}
|
||||
totalPages={volumes.hits.length}
|
||||
paginationHandlers={{
|
||||
nextPage: () => {},
|
||||
previousPage: () => {},
|
||||
}}
|
||||
columns={columnData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactElement, 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 SearchBar from "../Library/SearchBar";
|
||||
@@ -73,8 +73,8 @@ export const WantedComics = (props): ReactElement => {
|
||||
id: "downloadDetails",
|
||||
accessorKey: "acquisition",
|
||||
cell: data => <ol>
|
||||
{data.getValue().directconnect.downloads.map(download => {
|
||||
return <li className="is-size-7">{download.name}</li>;
|
||||
{data.getValue().directconnect.downloads.map((download, idx) => {
|
||||
return <li className="is-size-7" key={idx}>{download.name}</li>;
|
||||
})}
|
||||
</ol>
|
||||
},
|
||||
@@ -86,23 +86,81 @@ export const WantedComics = (props): ReactElement => {
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Pagination control that fetches the next x (pageSize) items
|
||||
* based on the y (pageIndex) offset from the Elasticsearch index
|
||||
* @param {number} pageIndex
|
||||
* @param {number} pageSize
|
||||
* @returns void
|
||||
*
|
||||
**/
|
||||
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
|
||||
dispatch(
|
||||
searchIssue(
|
||||
{
|
||||
query: {},
|
||||
},
|
||||
{
|
||||
pagination: {
|
||||
size: pageSize,
|
||||
from: pageSize * pageIndex + 1,
|
||||
},
|
||||
type: "wanted",
|
||||
trigger: "wantedComicsPage",
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
||||
/**
|
||||
* Pagination control that fetches the previous x (pageSize) items
|
||||
* based on the y (pageIndex) offset from the Elasticsearch index
|
||||
* @param {number} pageIndex
|
||||
* @param {number} pageSize
|
||||
* @returns void
|
||||
**/
|
||||
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
|
||||
let from = 0;
|
||||
if (pageIndex === 2) {
|
||||
from = (pageIndex - 1) * pageSize + 2 - 17;
|
||||
} else {
|
||||
from = (pageIndex - 1) * pageSize + 2 - 16;
|
||||
}
|
||||
dispatch(
|
||||
searchIssue(
|
||||
{
|
||||
query: {},
|
||||
},
|
||||
{
|
||||
pagination: {
|
||||
size: pageSize,
|
||||
from,
|
||||
},
|
||||
type: "wanted",
|
||||
trigger: "wantedComicsPage"
|
||||
},
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<div className="section">
|
||||
<h1 className="title">Wanted Comics</h1>
|
||||
{/* Search bar */}
|
||||
<SearchBar />
|
||||
<div className="header-area">
|
||||
<h1 className="title">Wanted Comics</h1>
|
||||
</div>
|
||||
{!isEmpty(wantedComics) && (
|
||||
<div>
|
||||
<div className="library">
|
||||
<T2Table
|
||||
rowData={wantedComics}
|
||||
sourceData={wantedComics}
|
||||
totalPages={wantedComics.length}
|
||||
columns={columnData}
|
||||
// paginationHandlers={{
|
||||
// nextPage: goToNextPage,
|
||||
// previousPage: goToPreviousPage,
|
||||
// }}
|
||||
paginationHandlers={{
|
||||
nextPage: nextPage,
|
||||
previousPage: previousPage,
|
||||
}}
|
||||
// rowClickHandler={navigateToComicDetail}
|
||||
/>
|
||||
{/* pagination controls */}
|
||||
|
||||
@@ -57,6 +57,12 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
||||
>
|
||||
{rawFileDetails.extension}
|
||||
</span>
|
||||
<span
|
||||
className="tag is-success is-light has-text-weight-semibold"
|
||||
style={props.tagsStyle}
|
||||
>
|
||||
{rawFileDetails.mimeType}
|
||||
</span>
|
||||
<span
|
||||
className="tag is-success is-light"
|
||||
style={props.tagsStyle}
|
||||
@@ -77,7 +83,9 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
),
|
||||
@@ -211,7 +219,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
||||
orientation={"vertical"}
|
||||
hasDetails={false}
|
||||
imageStyle={props.imageStyle}
|
||||
// cardContainerStyle={{ maxWidth: 200 }}
|
||||
// cardContainerStyle={{ maxWidth: 200 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="column">{metadataPanel.content()}</div>
|
||||
|
||||
@@ -1,27 +1,112 @@
|
||||
import React, { ReactElement, useState } from "react";
|
||||
import React, { ReactElement, useMemo, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import SearchBar from "../Library/SearchBar";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
useReactTable,
|
||||
PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
export const T2Table = (tableOptions): ReactElement => {
|
||||
const { rowData, columns, paginationHandlers, totalPages, rowClickHandler } =
|
||||
const { sourceData, columns, paginationHandlers: { nextPage, previousPage }, totalPages, rowClickHandler } =
|
||||
tableOptions;
|
||||
const [isPageSizeDropdownCollapsed, collapsePageSizeDropdown] =
|
||||
useState(false);
|
||||
const togglePageSizeDropdown = () =>
|
||||
collapsePageSizeDropdown(!isPageSizeDropdownCollapsed);
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] =
|
||||
useState<PaginationState>({
|
||||
pageIndex: 1,
|
||||
pageSize: 15,
|
||||
});
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
);
|
||||
|
||||
/**
|
||||
* Pagination control to move forward one page
|
||||
* @returns void
|
||||
*/
|
||||
const goToNextPage = () => {
|
||||
setPagination({
|
||||
pageIndex: pageIndex + 1,
|
||||
pageSize,
|
||||
});
|
||||
nextPage(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination control to move backward one page
|
||||
* @returns void
|
||||
**/
|
||||
const goToPreviousPage = () => {
|
||||
setPagination({
|
||||
pageIndex: pageIndex - 1,
|
||||
pageSize,
|
||||
});
|
||||
previousPage(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: rowData,
|
||||
data: sourceData,
|
||||
columns,
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
pageCount: sourceData.length ?? -1,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="columns table-controls">
|
||||
{/* Search bar */}
|
||||
<div className="column is-half">
|
||||
<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">
|
||||
<div className="control">
|
||||
<div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div>
|
||||
</div>
|
||||
|
||||
<div className="field has-addons ml-5">
|
||||
<p className="control">
|
||||
<button className="button">
|
||||
<span className="icon is-small">
|
||||
<i className="fa-solid fa-list"></i>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p className="control">
|
||||
<button className="button">
|
||||
<Link to="/library-grid">
|
||||
<span className="icon is-small">
|
||||
<i className="fa-solid fa-image"></i>
|
||||
</span>
|
||||
</Link>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<table className="table is-hoverable">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup, idx) => (
|
||||
@@ -60,15 +145,12 @@ export const T2Table = (tableOptions): ReactElement => {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* pagination control */}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
T2Table.propTypes = {
|
||||
rowData: PropTypes.array,
|
||||
sourceData: PropTypes.array,
|
||||
totalPages: PropTypes.number,
|
||||
columns: PropTypes.array,
|
||||
paginationHandlers: PropTypes.shape({
|
||||
|
||||
@@ -51,6 +51,7 @@ export const IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS =
|
||||
"IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS";
|
||||
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
|
||||
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
|
||||
export const VOLUMES_FETCHED="VOLUMES_FETCHED";
|
||||
|
||||
// search results from the Search service
|
||||
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";
|
||||
@@ -80,6 +81,8 @@ export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS =
|
||||
export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED =
|
||||
"IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED";
|
||||
|
||||
export const COMICBOOK_EXTRACTION_SUCCESS = "COMICBOOK_EXTRACTION_SUCCESS";
|
||||
|
||||
// Image file stats
|
||||
export const IMG_ANALYSIS_CALL_IN_PROGRESS = "IMG_ANALYSIS_CALL_IN_PROGRESS";
|
||||
export const IMG_ANALYSIS_DATA_FETCH_SUCCESS =
|
||||
@@ -112,6 +115,8 @@ export const AIRDCPP_FILE_DOWNLOAD_COMPLETED =
|
||||
export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
|
||||
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
|
||||
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
|
||||
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
|
||||
@@ -134,4 +139,4 @@ export const SETTINGS_DB_FLUSH_SUCCESS = "SETTINGS_DB_FLUSH_SUCCESS";
|
||||
// Metron Metadata
|
||||
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_ERROR = "METRON_DATA_FETCH_ERROR";
|
||||
export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR";
|
||||
@@ -8,71 +8,72 @@ export const hostURIBuilder = (options: Record<string, string>): string => {
|
||||
options.apiPath
|
||||
);
|
||||
};
|
||||
console.log(import.meta);
|
||||
|
||||
export const CORS_PROXY_SERVER_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "8050",
|
||||
apiPath: "/",
|
||||
});
|
||||
|
||||
export const COMICVINE_SERVICE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3080",
|
||||
apiPath: "/api/comicvine",
|
||||
});
|
||||
|
||||
export const METRON_SERVICE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3080",
|
||||
apiPath: "/api/metron",
|
||||
});
|
||||
|
||||
export const API_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "8050",
|
||||
apiPath: "/api",
|
||||
});
|
||||
|
||||
export const LIBRARY_SERVICE_HOST = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3000",
|
||||
apiPath: ``,
|
||||
});
|
||||
export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3000",
|
||||
apiPath: "/api/library",
|
||||
});
|
||||
export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3000",
|
||||
apiPath: "/api/search",
|
||||
});
|
||||
|
||||
export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3000",
|
||||
apiPath: "/api/settings",
|
||||
});
|
||||
|
||||
export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3000",
|
||||
apiPath: "/api/imagetransformation",
|
||||
});
|
||||
|
||||
export const SOCKET_BASE_URI = hostURIBuilder({
|
||||
protocol: "ws",
|
||||
host: process.env.UNDERLYING_HOSTNAME || "localhost",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
port: "3001",
|
||||
apiPath: `/`,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { isEmpty, isUndefined } from "lodash";
|
||||
import React, { createContext, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { toggleAirDCPPSocketConnectionStatus } from "../actions/airdcpp.actions";
|
||||
import { getSettings } from "../actions/settings.actions";
|
||||
|
||||
import AirDCPPSocket from "../services/DcppSearchService";
|
||||
|
||||
const AirDCPPSocketContextProvider = ({ children }) => {
|
||||
@@ -12,7 +14,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
|
||||
airDCPPState: {
|
||||
settings: settingsObject,
|
||||
socket: {},
|
||||
socketConnectionInformation: {},
|
||||
socketConectionInformation: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -51,16 +53,35 @@ const AirDCPPSocketContextProvider = ({ children }) => {
|
||||
client: { host },
|
||||
},
|
||||
} = configuration;
|
||||
|
||||
const initializedAirDCPPSocket = new AirDCPPSocket({
|
||||
protocol: `${host.protocol}`,
|
||||
hostname: `${host.hostname}`,
|
||||
hostname: `${host.hostname}:${host.port}`,
|
||||
username: `${host.username}`,
|
||||
password: `${host.password}`,
|
||||
});
|
||||
|
||||
const socketConnectionInformation = await initializedAirDCPPSocket.connect(
|
||||
`${host.username}`,
|
||||
`${host.password}`,
|
||||
true,
|
||||
);
|
||||
// connect and disconnect handlers
|
||||
initializedAirDCPPSocket.onConnected = (sessionInfo) => {
|
||||
dispatch(toggleAirDCPPSocketConnectionStatus("connected", sessionInfo));
|
||||
};
|
||||
initializedAirDCPPSocket.onDisconnected = async (
|
||||
reason,
|
||||
code,
|
||||
wasClean,
|
||||
) => {
|
||||
dispatch(
|
||||
toggleAirDCPPSocketConnectionStatus("disconnected", {
|
||||
reason,
|
||||
code,
|
||||
wasClean,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const socketConnectionInformation = await initializedAirDCPPSocket.connect();
|
||||
|
||||
// update the state with the new socket connection information
|
||||
persistSettings({
|
||||
...airDCPPState,
|
||||
airDCPPState: {
|
||||
@@ -80,7 +101,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
|
||||
};
|
||||
const AirDCPPSocketContext = createContext({
|
||||
airDCPPState: {},
|
||||
saveSettings: () => {},
|
||||
saveSettings: () => { },
|
||||
});
|
||||
|
||||
export { AirDCPPSocketContext, AirDCPPSocketContextProvider };
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
AIRDCPP_BUNDLES_FETCHED,
|
||||
AIRDCPP_TRANSFERS_FETCHED,
|
||||
LIBRARY_ISSUE_BUNDLES,
|
||||
AIRDCPP_SOCKET_CONNECTED,
|
||||
AIRDCPP_SOCKET_DISCONNECTED,
|
||||
} from "../constants/action-types";
|
||||
import { LOCATION_CHANGE } from "redux-first-history";
|
||||
import { isNil, isUndefined } from "lodash";
|
||||
@@ -24,6 +26,9 @@ const initialState = {
|
||||
downloadFileStatus: {},
|
||||
bundles: [],
|
||||
transfers: [],
|
||||
isAirDCPPSocketConnected: false,
|
||||
airDCPPSessionInfo: {},
|
||||
socketDisconnectionReason: {},
|
||||
};
|
||||
|
||||
function airdcppReducer(state = initialState, action) {
|
||||
@@ -95,8 +100,23 @@ function airdcppReducer(state = initialState, action) {
|
||||
...state,
|
||||
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:
|
||||
return {
|
||||
...state,
|
||||
searchResults: [],
|
||||
isAirDCPPSearchInProgress: false,
|
||||
searchInfo: null,
|
||||
@@ -105,7 +125,6 @@ function airdcppReducer(state = initialState, action) {
|
||||
bundleDBImportResult: null,
|
||||
// bundles: [],
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -109,10 +109,14 @@ function comicinfoReducer(state = initialState, action) {
|
||||
...state,
|
||||
};
|
||||
case CV_WEEKLY_PULLLIST_FETCHED: {
|
||||
const foo = [];
|
||||
action.data.map((item) => {
|
||||
foo.push({issue: item})
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
inProgress: false,
|
||||
pullList: [...action.data],
|
||||
pullList: foo,
|
||||
};
|
||||
}
|
||||
case LIBRARY_STATISTICS_CALL_IN_PROGRESS:
|
||||
|
||||
@@ -28,7 +28,12 @@ import {
|
||||
LS_IMPORT_CALL_IN_PROGRESS,
|
||||
SS_SEARCH_FAILED,
|
||||
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||
VOLUMES_FETCHED,
|
||||
COMICBOOK_EXTRACTION_SUCCESS,
|
||||
} from "../constants/action-types";
|
||||
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
|
||||
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
|
||||
|
||||
const initialState = {
|
||||
IMSCallInProgress: false,
|
||||
IMGCallInProgress: false,
|
||||
@@ -41,10 +46,14 @@ const initialState = {
|
||||
isComicVineMetadataImportInProgress: false,
|
||||
comicVineMetadataImportError: {},
|
||||
rawImportError: {},
|
||||
extractedComicBookArchive: [],
|
||||
extractedComicBookArchive: {
|
||||
reading: [],
|
||||
analysis: [],
|
||||
},
|
||||
recentComics: [],
|
||||
wantedComics: [],
|
||||
libraryComics: [],
|
||||
volumes: [],
|
||||
librarySearchResultsFormatted: [],
|
||||
librarySearchResultCount: 0,
|
||||
libraryQueueResults: [],
|
||||
@@ -79,7 +88,7 @@ function fileOpsReducer(state = initialState, action) {
|
||||
case IMS_RECENT_COMICS_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
recentComics: action.data,
|
||||
recentComics: action.data.docs,
|
||||
};
|
||||
case IMS_WANTED_COMICS_FETCHED:
|
||||
return {
|
||||
@@ -129,13 +138,7 @@ function fileOpsReducer(state = initialState, action) {
|
||||
comicBookExtractionInProgress: true,
|
||||
};
|
||||
}
|
||||
case IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
extractedComicBookArchive: action.extractedComicBookArchive,
|
||||
comicBookExtractionInProgress: false,
|
||||
};
|
||||
}
|
||||
|
||||
case LOCATION_CHANGE: {
|
||||
return {
|
||||
...state,
|
||||
@@ -150,11 +153,44 @@ function fileOpsReducer(state = initialState, action) {
|
||||
}
|
||||
case LS_COVER_EXTRACTED: {
|
||||
console.log("BASH", action);
|
||||
if(state.recentComics.length === 5) {
|
||||
state.recentComics.pop();
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
librarySearchResultCount: state.librarySearchResultCount + 1,
|
||||
recentComics: [...state.recentComics, action.result.data.importResult]
|
||||
};
|
||||
}
|
||||
|
||||
case COMICBOOK_EXTRACTION_SUCCESS: {
|
||||
const comicBookPages: string[] = [];
|
||||
map(action.result.files, (page) => {
|
||||
const pageFilePath = removeLeadingPeriod(page);
|
||||
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
|
||||
comicBookPages.push(imagePath);
|
||||
});
|
||||
|
||||
switch (action.result.purpose) {
|
||||
case "reading":
|
||||
return {
|
||||
...state,
|
||||
extractedComicBookArchive: {
|
||||
reading: comicBookPages,
|
||||
},
|
||||
comicBookExtractionInProgress: false,
|
||||
};
|
||||
|
||||
case "analysis":
|
||||
return {
|
||||
...state,
|
||||
extractedComicBookArchive: {
|
||||
analysis: comicBookPages,
|
||||
},
|
||||
comicBookExtractionInProgress: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
case LS_QUEUE_DRAINED: {
|
||||
console.log("drained", action);
|
||||
return {
|
||||
@@ -196,9 +232,8 @@ function fileOpsReducer(state = initialState, action) {
|
||||
}
|
||||
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
|
||||
const foo = [];
|
||||
console.log(action.data.hits)
|
||||
if (!isUndefined(action.data.hits)) {
|
||||
map(action.data.hits.hits, ({ _source }) => {
|
||||
map(action.data.hits, ({ _source }) => {
|
||||
foo.push(_source);
|
||||
});
|
||||
}
|
||||
@@ -211,7 +246,7 @@ function fileOpsReducer(state = initialState, action) {
|
||||
case WANTED_COMICS_FETCHED: {
|
||||
const foo = [];
|
||||
if (!isUndefined(action.data.hits)) {
|
||||
map(action.data.hits.hits, ({ _source }) => {
|
||||
map(action.data.hits, ({ _source }) => {
|
||||
foo.push(_source);
|
||||
});
|
||||
}
|
||||
@@ -221,7 +256,14 @@ function fileOpsReducer(state = initialState, action) {
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
case VOLUMES_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
volumes: action.data,
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
|
||||
case SS_SEARCH_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
@@ -241,4 +283,4 @@ function fileOpsReducer(state = initialState, action) {
|
||||
}
|
||||
}
|
||||
|
||||
export default fileOpsReducer;
|
||||
export default fileOpsReducer;
|
||||
@@ -10,7 +10,7 @@ class AirDCPPSocket {
|
||||
}
|
||||
const options = {
|
||||
url: `${socketProtocol}://${configuration.hostname}/api/v1/`,
|
||||
autoReconnect: false,
|
||||
autoReconnect: true,
|
||||
reconnectInterval: 5,
|
||||
logLevel: "verbose",
|
||||
ignoredListenerEvents: [
|
||||
@@ -18,6 +18,8 @@ class AirDCPPSocket {
|
||||
"hash_statistics",
|
||||
"hub_counts_updated",
|
||||
],
|
||||
username: `${configuration.username}`,
|
||||
password: `${configuration.password}`,
|
||||
};
|
||||
const AirDCPPSocketInstance = Socket(options, window.WebSocket as any);
|
||||
return AirDCPPSocketInstance;
|
||||
|
||||
@@ -18,7 +18,7 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
|
||||
}
|
||||
|
||||
export const search = async (data: SearchData) => {
|
||||
await SocketService.connect("admin", "password");
|
||||
await SocketService.connect();
|
||||
const instance: SearchInstance = await SocketService.post("search");
|
||||
const unsubscribe = await SocketService.addListener(
|
||||
"search",
|
||||
|
||||
@@ -67,7 +67,10 @@ export const determineCoverFile = (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const determineExternalMetadata = (metadataSource, source) => {
|
||||
export const determineExternalMetadata = (
|
||||
metadataSource: string,
|
||||
source: any
|
||||
) => {
|
||||
switch (metadataSource) {
|
||||
case "comicvine":
|
||||
return {
|
||||
@@ -87,4 +90,4 @@ export const determineExternalMetadata = (metadataSource, source) => {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -30,20 +30,20 @@ RawFileDetails.args = {
|
||||
},
|
||||
"inferredMetadata": {
|
||||
"issue": {
|
||||
"name": "Fantastic Four 143",
|
||||
"name": "Batman Annual 02",
|
||||
"number": 197402,
|
||||
"year": "1974",
|
||||
"subtitle": ""
|
||||
}
|
||||
},
|
||||
"rawFileDetails": {
|
||||
"name": "197402 Fantastic Four 143",
|
||||
"name": "Batman Annual 02",
|
||||
"filePath": "/Users/rishi/work/threetwo-core-service/comics/197402 Fantastic Four 143.cbz",
|
||||
"fileSize": 10341074,
|
||||
"extension": ".cbz",
|
||||
"containedIn": "./userdata/covers/197402 Fantastic Four 143",
|
||||
"cover": {
|
||||
"filePath": "userdata/covers/197402 Fantastic Four 143/FF143_01.jpg"
|
||||
"filePath": "userdata/covers/Batman Annual 02/Batman 002_00a.jpg"
|
||||
}
|
||||
},
|
||||
"sourcedMetadata": { "comicInfo": null, "comicvine": { "aliases": [] } },
|
||||
|
||||
@@ -30,12 +30,12 @@ app.use(bodyParser.urlencoded({ extended: true }));
|
||||
// REGISTER ROUTES
|
||||
// all of the routes will be prefixed with /api
|
||||
const routes: Router[] = Object.values(router);
|
||||
app.use("/api", routes);
|
||||
// app.use("/api", routes);
|
||||
|
||||
// Send index.html on root request
|
||||
app.use(express.static("dist"));
|
||||
app.use(express.static("public"));
|
||||
// app.use(express.static("dist"));
|
||||
// app.use(express.static("public"));
|
||||
|
||||
app.listen(port);
|
||||
// app.listen(port);
|
||||
|
||||
console.log(`Server is listening on ${port}`);
|
||||
// console.log(`Server is listening on ${port}`);
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": false,
|
||||
"target": "es2019",
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist/",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"strict": true
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"settings": {
|
||||
"eslint.workingDirectories": [
|
||||
|
||||
23
vite.config.js
Normal file
23
vite.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
publicDir: "public",
|
||||
base: "",
|
||||
build: "esnext",
|
||||
server: { host: true },
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
// browser: true
|
||||
exportConditions: ["node"],
|
||||
}),
|
||||
react({
|
||||
// Use React plugin in all *.jsx and *.tsx files
|
||||
include: "**/*.{jsx,tsx}",
|
||||
babel: {
|
||||
plugins: ["babel-plugin-styled-components"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const webpack = require("webpack");
|
||||
const outputDirectory = "dist";
|
||||
const BundleAnalyzerPlugin =
|
||||
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
|
||||
module.exports = (_, argv) => {
|
||||
const mode = argv.mode;
|
||||
const isDevelopment = mode === "development";
|
||||
return {
|
||||
entry: ["babel-polyfill", "./src/client/index.tsx"],
|
||||
output: {
|
||||
path: path.resolve(__dirname, outputDirectory),
|
||||
filename: "./js/[name].bundle.js",
|
||||
},
|
||||
devtool: "source-map",
|
||||
stats: {
|
||||
children: true,
|
||||
},
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.(js|ts)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: "comlink-loader",
|
||||
options: {
|
||||
singleton: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: [/\.js?$/, /\.jsx?$/, /\.tsx?$/],
|
||||
use: [
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
plugins: [
|
||||
isDevelopment && require("react-refresh/babel"),
|
||||
].filter(Boolean),
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
enforce: "pre",
|
||||
test: /\.js$/,
|
||||
loader: "source-map-loader",
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.s[ac]ss$/i,
|
||||
use: [
|
||||
// Creates `style` nodes from JS strings
|
||||
"style-loader",
|
||||
// Translates CSS into CommonJS
|
||||
"css-loader",
|
||||
// Compiles Sass to CSS
|
||||
"sass-loader",
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/i,
|
||||
use: [
|
||||
"file-loader?hash=sha512&digest=hex&name=img/[contenthash].[ext]",
|
||||
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ["*", ".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||
aliasFields: ["browser", "browser.esm"],
|
||||
},
|
||||
mode: process.env.NODE_ENV,
|
||||
devServer: {
|
||||
hot: true,
|
||||
port: 3050,
|
||||
open: true,
|
||||
proxy: {
|
||||
"/api/**": {
|
||||
target: "http://localhost:8050",
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
usedExports: false,
|
||||
},
|
||||
plugins: [
|
||||
// new BundleAnalyzerPlugin(),
|
||||
new CopyPlugin({
|
||||
patterns: [{ from: "./src/client/assets/img/", to: "img/" }],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
},
|
||||
}),
|
||||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./public/index.html",
|
||||
favicon: "./public/favicon.ico",
|
||||
title: "express-typescript-react",
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.UNDERLYING_HOSTNAME": JSON.stringify(
|
||||
process.env.UNDERLYING_HOSTNAME,
|
||||
),
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "./css/[name].css",
|
||||
chunkFilename: "./css/[id].css",
|
||||
}),
|
||||
].filter(Boolean),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user