Compare commits

...

20 Commits

Author SHA1 Message Date
d72f666a51 🔧 Refactor for axios upgrade 2023-03-25 16:31:24 -04:00
a939bf4c71 🐛UI bugs (#68)
* 🔧 Updated date for PullList on Dashboard

* 🔧 Fixes for broken image paths

* ⬆️ Bumped deps, removed useless ones
2023-03-24 00:00:48 -04:00
0a48ecbb2c 📗 Upgraded Storybook to v7 rc (#69) 2023-03-15 16:43:16 -04:00
c5dd1abcdd 🖼️ Relocating screenshots (#70)
* 🔧 Updated date for PullList on Dashboard

* 🔧 Fixes for broken image paths

* 🖼️ Updated screenshot links in README
2023-03-15 14:04:49 -04:00
79f9b22fad Elasticsearch upgrade fix (#67)
* 🔧 Fixed the response object in reducers and components https://github.com/rishighan/threetwo/issues/64

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* #️⃣ Added a key prop to MetadataPanel in global search results

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed DOMNesting issues

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed the response in wanted reducer action

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

*  Committing the reducer

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixes for DOMNesting issues on the Downloads page

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2023-03-13 11:25:15 -04:00
dependabot[bot]
e291b4806b Bump http-cache-semantics from 4.1.0 to 4.1.1 (#65)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-02 21:47:16 -05:00
ff7b7c9207 Fix for elasticsearch upgrade breakage (#66)
* 🔧 Fixed the response object in reducers and components https://github.com/rishighan/threetwo/issues/64

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* #️⃣ Added a key prop to MetadataPanel in global search results

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed DOMNesting issues

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixed the response in wanted reducer action

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

*  Committing the reducer

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

* 🔧 Fixes for DOMNesting issues on the Downloads page

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2023-03-02 00:32:43 -05:00
2d61fa1436 Changing the build system to Vite (#62)
* 🔧 Updated date for PullList on Dashboard

* ️ Added Vite config and removed useless files

* 👷🏽 Updated build command

*  Removed useless deps

* 🔧 Cleaned up package.json and bumped airdcpp-apisocket

* 🔧 Updated some packages and deps

* ⬆️ Bumped some deps

* 🔧 Fixed typo in package.json

* 🔧 Fix for broken paths https://github.com/rishighan/threetwo/issues/63

* 🔧 Fixed broken path and npm script

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>

---------

Signed-off-by: Rishi Ghan <rishi.ghan@gmail.com>
2023-03-01 23:31:22 -05:00
b12849efda 🔧 Fixed the path to funding.yml (#61) 2023-01-28 09:42:39 -08:00
fb2c969c00 💵 Added a funding page (#60) 2023-01-28 09:28:22 -08:00
d437fddd1a Merge branch 'master' of https://github.com/rishighan/threetwo 2023-01-21 18:31:00 -08:00
8dd68e9d54 AirDC++ Socket Status (#58) 2023-01-21 02:29:32 -08:00
dependabot[bot]
a8b52d0ac6 Bump json5 from 1.0.1 to 1.0.2 (#56)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-08 08:49:09 -08:00
719ebe7c6e ThreeTwo favicon (#54)
* 🔧 Fixed empty library state with explanation

* ️ Added a status indicator for the AirDC++ socket connection

* 🖼️ Fixed the ThreeTwo favico
2023-01-01 17:14:27 -08:00
ddef87ea29 AirDC++ Connection Status (#53)
* 🔧 Fixed empty library state with explanation

* ️ Added a status indicator for the AirDC++ socket connection
2022-12-30 22:39:38 -08:00
71bad167ab 🔧 Library zero state (#52)
* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README

* 🏗️ Massive refactor around archive uncompression for reading/analysis

* 🔧 Tweaked state vars for reading and analysis

* 🏗️ Refactor to support DC++ and socket.io integration

This refactor covers the following workflows:

1. Adding a comic from LOCG or ComicVine adds it to the wanted list
2. Downloading that comic from DC++ correctly adds download metadata to the corresponding comic object in mongo
3. Successful download triggers automatic import to library and cover extraction, metadata application
2022-12-29 15:55:33 -08:00
df1fbc7dd3 🔧 Fixed empty library state with explanation 2022-12-29 23:33:23 +00:00
789e5b9518 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-12-21 21:49:42 -08:00
ce6653b5d7 🏗️ Applying the refactor patc 2022-12-21 21:49:12 -08:00
d065225d8e 🏗️ Refactoring archive uncompression for "Read Comic" and "Analysis" user flows (#46)
* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README

* 🏗️ Massive refactor around archive uncompression for reading/analysis

* 🔧 Tweaked state vars for reading and analysis

* 🏗️ Refactor to support DC++ and socket.io integration

This refactor covers the following workflows:

1. Adding a comic from LOCG or ComicVine adds it to the wanted list
2. Downloading that comic from DC++ correctly adds download metadata to the corresponding comic object in mongo
3. Successful download triggers automatic import to library and cover extraction, metadata application
2022-12-21 21:17:38 -08:00
52 changed files with 3958 additions and 9089 deletions

View File

@@ -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"]
}
}
}

View File

@@ -1,14 +1,14 @@
module.exports = { module.exports = {
extends: ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:css-modules/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended"], extends: ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:css-modules/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended"],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
sourceType: "module", sourceType: "module",
ecmaVersion: 2020, ecmaVersion: 2020,
ecmaFeatures: { ecmaFeatures: {
jsx: true // Allows for the parsing of JSX jsx: true // Allows for the parsing of JSX
} }
}, },
plugins: ["@typescript-eslint", "css-modules"], plugins: ["@typescript-eslint", "css-modules"],
settings: { settings: {
"import/resolver": { "import/resolver": {
@@ -18,9 +18,9 @@ module.exports = {
}, },
react: { react: {
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
} }
}, },
// Fine tune rules // Fine tune rules
rules: { rules: {
"@typescript-eslint/no-var-requires": 0 "@typescript-eslint/no-var-requires": 0

View File

@@ -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
View 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;

View File

@@ -0,0 +1,3 @@
<script>
window.global = window;
</script>

View File

@@ -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
View File

@@ -0,0 +1,18 @@
import type { Preview } from "@storybook/react";
const preview: Preview = {
parameters: {
backgrounds: {
default: "light",
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -6,13 +6,13 @@ ThreeTwo! _aims to be_ a comic book curation app.
### Screenshots ### Screenshots
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/Dashboard.png) ![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/Dashboard.png)
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/Library.png) ![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/Library.png)
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/DC%2B%2B%20integration.png) ![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/DC%2B%2B%20integration.png)
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/ComicVine%20Matching.png) ![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/ComicVine%20Matching.png)
### 🦄 Early Development Support Channel ### 🦄 Early Development Support Channel

1
funding.yml Normal file
View File

@@ -0,0 +1 @@
github: [rishighan]

View File

@@ -10,6 +10,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/client/index.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -5,15 +5,12 @@
"main": "server/index.js", "main": "server/index.js",
"typings": "server/index.js", "typings": "server/index.js",
"scripts": { "scripts": {
"build": "webpack --mode production", "build": "vite build",
"start": "npm run build && npm run server", "dev": "rimraf dist && npm run build && vite",
"client": "webpack serve --mode development --devtool inline-source-map --hot", "prod": "npm run build && vite",
"server": "tsc -p tsconfig.server.json && node server/",
"dev": "concurrently \"nodemon\" \"npm run client\"",
"server-dev": "nodemon",
"docs": "jsdoc -c jsdoc.json", "docs": "jsdoc -c jsdoc.json",
"storybook": "start-storybook -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "build-storybook" "build-storybook": "storybook build"
}, },
"author": "Rishi Ghan", "author": "Rishi Ghan",
"license": "MIT", "license": "MIT",
@@ -23,79 +20,68 @@
"@dnd-kit/core": "^4.0.0", "@dnd-kit/core": "^4.0.0",
"@dnd-kit/sortable": "^5.0.0", "@dnd-kit/sortable": "^5.0.0",
"@dnd-kit/utilities": "^3.2.0", "@dnd-kit/utilities": "^3.2.0",
"@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-free": "^6.3.0",
"@parcel/config-default": "^2.6.0",
"@redux-devtools/extension": "^3.2.2", "@redux-devtools/extension": "^3.2.2",
"@tanstack/react-table": "^8.5.11", "@rollup/plugin-node-resolve": "^15.0.1",
"@tanstack/react-table": "^8.8.0",
"@types/axios": "^0.14.0",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/react": "^17.0.3", "@types/react-router-dom": "^5.3.3",
"@types/react-dom": "^17.0.2",
"@types/react-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7",
"@types/sharp": "^0.28.0",
"@types/socket.io": "^3.0.2", "@types/socket.io": "^3.0.2",
"@types/socket.io-client": "^3.0.0", "@types/socket.io-client": "^3.0.0",
"airdcpp-apisocket": "^2.4.4", "@vitejs/plugin-react": "^3.1.0",
"array-sort-by": "^1.2.1", "airdcpp-apisocket": "2.4.5-beta.1",
"axios": "^0.27.2", "axios": "^1.3.4",
"axios-cache-interceptor": "^1.0.1",
"axios-rate-limit": "^1.3.0", "axios-rate-limit": "^1.3.0",
"axios-simple-cache-adapter": "^1.1.0",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"babel-preset-minify": "^0.5.2", "babel-preset-minify": "^0.5.2",
"better-docs": "^2.7.2", "better-docs": "^2.7.2",
"comlink-loader": "^2.0.0",
"compromise": "^13.11.3",
"compromise-dates": "^2.2.1",
"compromise-numbers": "^1.4.0",
"compromise-sentences": "^0.3.0",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"ellipsize": "^0.1.0", "ellipsize": "^0.5.1",
"express": "^4.17.1", "express": "^4.17.1",
"fastest-validator": "^1.11.0",
"filename-parser": "^1.0.2", "filename-parser": "^1.0.2",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"final-form-arrays": "^3.0.2", "final-form-arrays": "^3.0.2",
"history": "^5.3.0",
"html-to-text": "^8.1.0", "html-to-text": "^8.1.0",
"jsdoc": "^3.6.10", "jsdoc": "^3.6.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"opds-extra": "^3.0.9", "node-sass": "npm:sass",
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"qs": "^6.10.5", "qs": "^6.10.5",
"react": "^18.1.0", "react": "^18.2.0",
"react-collapsible": "^2.9.0", "react-collapsible": "^2.9.0",
"react-comic-viewer": "^0.4.0", "react-comic-viewer": "^0.4.0",
"react-day-picker": "^8.0.6", "react-day-picker": "^8.6.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-final-form": "^6.5.9", "react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.3", "react-final-form-arrays": "^3.1.4",
"react-loader-spinner": "^4.0.0", "react-loader-spinner": "^4.0.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-redux": "^7.2.6", "react-redux": "^8.0.5",
"react-router": "^6.2.2", "react-router": "^6.9.0",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.9.0",
"react-select": "^5.3.2", "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-slick": "^0.29.0",
"react-sliding-pane": "^7.1.0", "react-sliding-pane": "^7.1.0",
"react-stickynode": "^4.0.0", "react-stickynode": "^4.1.0",
"react-textarea-autosize": "^8.3.4", "react-textarea-autosize": "^8.3.4",
"reapop": "^4.0.5", "reapop": "^4.2.1",
"redux-first-history": "^5.0.9", "redux-first-history": "^5.1.1",
"redux-socket.io-middleware": "^1.0.4", "redux-socket.io-middleware": "^1.0.4",
"redux-thunk": "^2.4.1", "redux-thunk": "^2.4.2",
"sharp": "^0.30.5",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
"socket.io-client": "^4.3.2", "socket.io-client": "^4.3.2",
"styled-components": "^5.3.5", "styled-components": "^5.3.9",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
"voca": "^1.4.0", "vite-plugin-html": "^3.2.0",
"websocket": "^1.0.34", "websocket": "^1.0.34",
"ws": "^7.5.3",
"xml2js": "^0.4.23",
"xregexp": "^5.0.2" "xregexp": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
@@ -103,45 +89,30 @@
"@babel/core": "^7.13.10", "@babel/core": "^7.13.10",
"@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.13.15", "@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.10", "@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.13.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@storybook/addon-essentials": "^7.0.0-rc.3",
"@storybook/addon-actions": "^6.5.9", "@storybook/addon-interactions": "^7.0.0-rc.3",
"@storybook/addon-essentials": "^6.5.9", "@storybook/addon-links": "^7.0.0-rc.3",
"@storybook/addon-interactions": "^6.5.9", "@storybook/blocks": "^7.0.0-rc.3",
"@storybook/addon-links": "^6.5.9", "@storybook/react": "^7.0.0-rc.3",
"@storybook/builder-webpack4": "^6.5.9", "@storybook/react-vite": "^7.0.0-rc.3",
"@storybook/builder-webpack5": "^6.5.9", "@storybook/testing-library": "^0.0.14-next.1",
"@storybook/manager-webpack4": "^6.5.9",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/preset-scss": "^1.0.3",
"@storybook/react": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/ellipsize": "^0.1.1",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@types/node": "^14.14.34", "@types/node": "^14.14.34",
"@types/react": "^17.0.3", "@types/react": "^18.0.28",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^18.0.11",
"@types/react-redux": "^7.1.16", "@types/react-redux": "^7.1.25",
"@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0", "@typescript-eslint/parser": "^4.17.0",
"awesome-typescript-loader": "^5.2.1",
"babel-eslint": "^10.0.0", "babel-eslint": "^10.0.0",
"babel-loader": "^8.2.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"buffer": "^6.0.3", "bulma": "^0.9.4",
"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",
"eslint": "^7.22.0", "eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1", "eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
@@ -151,35 +122,19 @@
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.22.0",
"eslint-plugin-storybook": "^0.6.11",
"express": "^4.17.1", "express": "^4.17.1",
"file-loader": "^6.2.0",
"fs-extra": "^9.1.0",
"html-webpack-plugin": "^5.3.1",
"http-response-stream": "^1.0.9",
"image-webpack-loader": "^8.1.0",
"install": "^0.13.0", "install": "^0.13.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"mini-css-extract-plugin": "^1.4.1",
"node-sass": "^7.0.1",
"nodemon": "^1.17.3", "nodemon": "^1.17.3",
"npm": "^8.11.0", "npm": "^8.11.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"rimraf": "^3.0.2", "rimraf": "^4.1.3",
"sass-loader": "^11.0.1", "sass": "^1.58.1",
"source-map-loader": "^0.2.4", "storybook": "^7.0.0-rc.3",
"string-similarity": "^4.0.4",
"style-loader": "^2.0.0",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "^4.2.3", "typescript": "^5.0.2",
"url-loader": "^1.0.1", "vite": "^4.2.0"
"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"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -4,7 +4,10 @@ import {
PriorityEnum, PriorityEnum,
SearchResponse, SearchResponse,
} from "threetwo-ui-typings"; } from "threetwo-ui-typings";
import { LIBRARY_SERVICE_BASE_URI, SEARCH_SERVICE_BASE_URI } from "../constants/endpoints"; import {
LIBRARY_SERVICE_BASE_URI,
SEARCH_SERVICE_BASE_URI,
} from "../constants/endpoints";
import { import {
AIRDCPP_SEARCH_RESULTS_ADDED, AIRDCPP_SEARCH_RESULTS_ADDED,
AIRDCPP_SEARCH_RESULTS_UPDATED, AIRDCPP_SEARCH_RESULTS_UPDATED,
@@ -18,6 +21,8 @@ import {
IMS_COMIC_BOOK_DB_OBJECT_FETCHED, IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
AIRDCPP_TRANSFERS_FETCHED, AIRDCPP_TRANSFERS_FETCHED,
LIBRARY_ISSUE_BUNDLES, LIBRARY_ISSUE_BUNDLES,
AIRDCPP_SOCKET_CONNECTED,
AIRDCPP_SOCKET_DISCONNECTED,
} from "../constants/action-types"; } from "../constants/action-types";
import { isNil } from "lodash"; import { isNil } from "lodash";
import axios from "axios"; import axios from "axios";
@@ -32,190 +37,199 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
return new Promise((resolve) => setTimeout(resolve, ms)); 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 = export const search =
(data: SearchData, ADCPPSocket: any, credentials: any) => (data: SearchData, ADCPPSocket: any, credentials: any) =>
async (dispatch) => { async (dispatch) => {
try { try {
if (!ADCPPSocket.isConnected()) { if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect( await ADCPPSocket();
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;
} }
}; 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 = export const downloadAirDCPPItem =
(searchInstanceId: Number, (
searchInstanceId: Number,
resultId: String, resultId: String,
comicObjectId: String, comicObjectId: String,
name: String, size: Number, type: any, name: String,
size: Number,
type: any,
ADCPPSocket: any, ADCPPSocket: any,
credentials: any, credentials: any,
): void => ): void =>
async (dispatch) => { async (dispatch) => {
try { try {
if (!ADCPPSocket.isConnected()) { if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect( await ADCPPSocket.connect();
`${credentials.username}`,
`${credentials.password}`,
true,
);
}
let bundleDBImportResult = {};
const downloadResult = await ADCPPSocket.post(
`search/${searchInstanceId}/results/${resultId}/download`,
);
if (!isNil(downloadResult)) {
bundleDBImportResult = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
bundleId: downloadResult.bundle_info.id,
comicObjectId,
name,
size,
type,
},
});
dispatch({
type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
downloadResult,
bundleDBImportResult,
});
dispatch({
type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
comicBookDetail: bundleDBImportResult.data,
IMS_inProgress: false,
});
}
} catch (error) {
throw error;
} }
}; let bundleDBImportResult = {};
const downloadResult = await ADCPPSocket.post(
`search/${searchInstanceId}/results/${resultId}/download`,
);
export const getBundlesForComic = if (!isNil(downloadResult)) {
(comicObjectId: string, ADCPPSocket: any, credentials: any) => bundleDBImportResult = await axios({
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
`${credentials.username}`,
`${credentials.password}`,
true,
);
}
const comicObject = await axios({
method: "POST", method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`, url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
}, },
data: { data: {
id: `${comicObjectId}`, bundleId: downloadResult.bundle_info.id,
comicObjectId,
name,
size,
type,
}, },
}); });
// get only the bundles applicable for the comic
if (comicObject.data.acquisition.directconnect) { dispatch({
const filteredBundles = comicObject.data.acquisition.directconnect.downloads.map( type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
downloadResult,
bundleDBImportResult,
});
dispatch({
type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
comicBookDetail: bundleDBImportResult.data,
IMS_inProgress: false,
});
}
} catch (error) {
throw error;
}
};
export const getBundlesForComic =
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect();
}
const comicObject = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
id: `${comicObjectId}`,
},
});
// get only the bundles applicable for the comic
if (comicObject.data.acquisition.directconnect) {
const filteredBundles =
comicObject.data.acquisition.directconnect.downloads.map(
async ({ bundleId }) => { async ({ bundleId }) => {
return await ADCPPSocket.get(`queue/bundles/${bundleId}`); return await ADCPPSocket.get(`queue/bundles/${bundleId}`);
}, },
); );
dispatch({ dispatch({
type: AIRDCPP_BUNDLES_FETCHED, type: AIRDCPP_BUNDLES_FETCHED,
bundles: await Promise.all(filteredBundles), bundles: await Promise.all(filteredBundles),
}); });
}
} catch (error) {
throw error;
} }
}; } catch (error) {
throw error;
}
};
export const getTransfers = export const getTransfers =
(ADCPPSocket: any, credentials: any) => async (dispatch) => { (ADCPPSocket: any, credentials: any) => async (dispatch) => {
try { try {
if (!ADCPPSocket.isConnected()) { if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect( await ADCPPSocket.connect();
`${credentials.username}`,
`${credentials.password}`,
true,
);
} }
const bundles = await ADCPPSocket.get("queue/bundles/1/85", {}); const bundles = await ADCPPSocket.get("queue/bundles/1/85", {});
if (!isNil(bundles)) { if (!isNil(bundles)) {
@@ -234,7 +248,6 @@ export const getTransfers =
type: LIBRARY_ISSUE_BUNDLES, type: LIBRARY_ISSUE_BUNDLES,
issue_bundles, issue_bundles,
}); });
} }
} catch (err) { } catch (err) {
throw err; throw err;

View File

@@ -1,9 +1,6 @@
import axios from "axios"; import axios from "axios";
import rateLimiter from "axios-rate-limit"; import rateLimiter from "axios-rate-limit";
import { import { setupCache } from "axios-cache-interceptor";
AxiosCacheRequestConfig,
createCacheAdapter,
} from "axios-simple-cache-adapter";
import qs from "qs"; import qs from "qs";
import { import {
CV_SEARCH_SUCCESS, CV_SEARCH_SUCCESS,
@@ -26,25 +23,22 @@ import {
LIBRARY_SERVICE_BASE_URI, LIBRARY_SERVICE_BASE_URI,
} from "../constants/endpoints"; } from "../constants/endpoints";
const axiosCacheAdapter = createCacheAdapter();
const http = rateLimiter(axios.create(), { const http = rateLimiter(axios.create(), {
maxRequests: 1, maxRequests: 1,
perMilliseconds: 1000, perMilliseconds: 1000,
maxRPS: 1, maxRPS: 1,
}); });
const cachedAxios = setupCache(axios);
export const getWeeklyPullList = (options) => async (dispatch) => { export const getWeeklyPullList = (options) => async (dispatch) => {
try { try {
dispatch({ dispatch({
type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS, type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
}); });
await axios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, { await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
method: "get", method: "get",
params: options, params: options,
axiosCacheAdapter, }).then((response) => {
cache: 1000, // value in MS
} as AxiosCacheRequestConfig).then((response) => {
dispatch({ dispatch({
type: CV_WEEKLY_PULLLIST_FETCHED, type: CV_WEEKLY_PULLLIST_FETCHED,
data: response.data.result, data: response.data.result,
@@ -61,6 +55,7 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
type: CV_API_CALL_IN_PROGRESS, type: CV_API_CALL_IN_PROGRESS,
inProgress: true, inProgress: true,
}); });
console.log(options);
const serviceURI = `${COMICVINE_SERVICE_URI}/${options.callURIAction}`; const serviceURI = `${COMICVINE_SERVICE_URI}/${options.callURIAction}`;
const response = await http(serviceURI, { const response = await http(serviceURI, {
method: options.callMethod, method: options.callMethod,
@@ -70,9 +65,11 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
"Content-Type": "application/json", "Content-Type": "application/json",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
}, },
paramsSerializer: (params) => { paramsSerializer: {
return qs.stringify(params, { arrayFormat: "repeat" }); indexes: null,
}, },
// (params) =>
// qs.stringify(params, { arrayFormat: "repeat" }),
}); });
switch (options.callURIAction) { switch (options.callURIAction) {
@@ -87,7 +84,7 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
console.log("Could not complete request."); console.log("Could not complete request.");
} }
} catch (error) { } catch (error) {
console.log(error); console.error(error);
dispatch({ dispatch({
type: CV_API_GENERIC_FAILURE, type: CV_API_GENERIC_FAILURE,
error, error,

View File

@@ -36,7 +36,7 @@ import {
CV_WEEKLY_PULLLIST_FETCHED, CV_WEEKLY_PULLLIST_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { success } from "react-notification-system-redux"; import { success } from "react-notification-system-redux";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { isNil, map } from "lodash"; import { isNil, map } from "lodash";
export async function walkFolder(path: string): Promise<Array<IFolderData>> { export async function walkFolder(path: string): Promise<Array<IFolderData>> {
@@ -60,15 +60,6 @@ export async function walkFolder(path: string): Promise<Array<IFolderData>> {
* @return the comic book metadata * @return the comic book metadata
*/ */
export const fetchComicBookMetadata = () => async (dispatch) => { export const fetchComicBookMetadata = () => async (dispatch) => {
const extractionOptions = {
extractTarget: "cover",
targetExtractionFolder: "./userdata/covers",
extractionMode: "bulk",
paginationOptions: {
pageLimit: 25,
page: 1,
},
};
dispatch({ dispatch({
type: LS_IMPORT_CALL_IN_PROGRESS, type: LS_IMPORT_CALL_IN_PROGRESS,
}); });
@@ -86,7 +77,7 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
dispatch({ dispatch({
type: LS_IMPORT, type: LS_IMPORT,
meta: { remote: true }, meta: { remote: true },
data: { extractionOptions }, data: {},
}); });
}; };
export const toggleImportQueueStatus = (options) => async (dispatch) => { export const toggleImportQueueStatus = (options) => async (dispatch) => {
@@ -136,21 +127,24 @@ export const getComicBooks = (options) => async (dispatch) => {
* @returns Nothing. * @returns Nothing.
* @param payload * @param payload
*/ */
export const importToDB = (sourceName: string, payload?: any) => (dispatch) => { export const importToDB = (sourceName: string, metadata?: any) => (dispatch) => {
try { try {
const comicBookMetadata = { const comicBookMetadata = {
rawFileDetails: { importType: "new",
name: "", payload: {
}, rawFileDetails: {
importStatus: { name: "",
isImported: true,
tagged: false,
matchedResult: {
score: "0",
}, },
}, importStatus: {
sourcedMetadata: payload || null, isImported: true,
acquisition: { source: { wanted: true, name: sourceName } }, tagged: false,
matchedResult: {
score: "0",
},
},
sourcedMetadata: metadata || null,
acquisition: { source: { wanted: true, name: sourceName } },
}
}; };
dispatch({ dispatch({
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS, type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
@@ -260,34 +254,31 @@ export const fetchComicVineMatches =
* @returns {any} * @returns {any}
*/ */
export const extractComicArchive = export const extractComicArchive =
(path: string, options: any): any => async (dispatch) => {
const comicBookPages: string[] = [];
console.log(options);
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,
options,
},
});
map(extractedComicBookArchive.data, (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,
});
};
(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) => { export const searchIssue = (query, options) => async (dispatch) => {
dispatch({ dispatch({
type: SS_SEARCH_IN_PROGRESS, type: SS_SEARCH_IN_PROGRESS,
@@ -310,26 +301,26 @@ export const searchIssue = (query, options) => async (dispatch) => {
case "wantedComicsPage": case "wantedComicsPage":
dispatch({ dispatch({
type: WANTED_COMICS_FETCHED, type: WANTED_COMICS_FETCHED,
data: response.data.body, data: response.data.hits,
}); });
break; break;
case "globalSearchBar": case "globalSearchBar":
dispatch({ dispatch({
type: SS_SEARCH_RESULTS_FETCHED_SPECIAL, type: SS_SEARCH_RESULTS_FETCHED_SPECIAL,
data: response.data.body, data: response.data.hits,
}); });
break; break;
case "libraryPage": case "libraryPage":
dispatch({ dispatch({
type: SS_SEARCH_RESULTS_FETCHED, type: SS_SEARCH_RESULTS_FETCHED,
data: response.data.body, data: response.data.hits,
}); });
break; break;
case "volumesPage": case "volumesPage":
dispatch({ dispatch({
type: VOLUMES_FETCHED, type: VOLUMES_FETCHED,
data: response.data.body, data: response.data.hits,
}); });
break; break;
@@ -358,4 +349,4 @@ export const analyzeImage =
type: IMG_ANALYSIS_DATA_FETCH_SUCCESS, type: IMG_ANALYSIS_DATA_FETCH_SUCCESS,
result: foo.data, result: foo.data,
}); });
}; };

View File

@@ -1,8 +1,8 @@
@import "../../../../node_modules/bulma/bulma.sass"; @import "/node_modules/bulma/bulma.sass";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; $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/fontawesome.scss";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss"; @import "/node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss"; @import "/node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
$bg-color: yellow; $bg-color: yellow;
$border-color: red; $border-color: red;
@@ -62,6 +62,10 @@ pre {
margin-left: -300px; margin-left: -300px;
min-width: 500px; min-width: 500px;
} }
.airdcpp-status {
min-width: 300px;
line-height: 1.7rem;
}
body { body {
background: #454a59; background: #454a59;
} }

View File

@@ -18,18 +18,22 @@ import {
AirDCPPSocketContext, AirDCPPSocketContext,
} from "../context/AirDCPPSocket"; } from "../context/AirDCPPSocket";
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types"; import {
import { useDispatch } from "react-redux"; AIRDCPP_DOWNLOAD_PROGRESS_TICK,
LS_SINGLE_IMPORT,
} from "../constants/action-types";
import { useDispatch, useSelector } from "react-redux";
/** /**
* Method that initializes an AirDC++ socket connection * Method that initializes an AirDC++ socket connection
* 1. Initializes event listeners for download init, tick and complete events * 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 * 2. Handles errors in case the connection to AirDC++ is not established or terminated
* @returns void * @returns void
*/ */
const AirDCPPSocketComponent = (): ReactElement => { const AirDCPPSocketComponent = (): ReactElement => {
const airDCPPConfiguration = useContext(AirDCPPSocketContext); const airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
const initializeAirDCPPEventListeners = async () => { const initializeAirDCPPEventListeners = async () => {
if ( if (
@@ -42,9 +46,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
"queue_bundle_added", "queue_bundle_added",
async (data) => { async (data) => {
console.log("JEMEN:", data); console.log("JEMEN:", data);
},
}
); );
// download tick listener // download tick listener
await airDCPPConfiguration.airDCPPState.socket.addListener( await airDCPPConfiguration.airDCPPState.socket.addListener(
@@ -62,9 +64,18 @@ const AirDCPPSocketComponent = (): ReactElement => {
`queue`, `queue`,
"queue_bundle_status", "queue_bundle_status",
async (bundleData) => { async (bundleData) => {
let count = 0;
if (bundleData.status.completed && bundleData.status.downloaded) { if (bundleData.status.completed && bundleData.status.downloaded) {
// dispatch the action for raw import, with the metadata // 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;
}
} }
}, },
); );
@@ -92,7 +103,10 @@ export const App = (): ReactElement => {
<Routes> <Routes>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} /> <Route path="/import" element={<Import path={"./comics"} />} />
<Route path="/library" element={<TabulatedContentContainer category="library" />} /> <Route
path="/library"
element={<TabulatedContentContainer category="library" />}
/>
<Route path="/library-grid" element={<LibraryGrid />} /> <Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} /> <Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
@@ -105,9 +119,18 @@ export const App = (): ReactElement => {
element={<VolumeDetail />} element={<VolumeDetail />}
/> />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/pull-list/all" element={<TabulatedContentContainer category="pullList" />} /> <Route
<Route path="/wanted/all" element={<TabulatedContentContainer category="wanted" />} /> path="/pull-list/all"
<Route path="/volumes/all" element={<TabulatedContentContainer category="volumes" />} /> element={<TabulatedContentContainer category="pullList" />}
/>
<Route
path="/wanted/all"
element={<TabulatedContentContainer category="wanted" />}
/>
<Route
path="/volumes/all"
element={<TabulatedContentContainer category="volumes" />}
/>
</Routes> </Routes>
</div> </div>
</AirDCPPSocketContextProvider> </AirDCPPSocketContextProvider>

View File

@@ -16,6 +16,7 @@ import ellipsize from "ellipsize";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { isEmpty, isNil, map } from "lodash"; import { isEmpty, isNil, map } from "lodash";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
interface IAcquisitionPanelProps { interface IAcquisitionPanelProps {
query: any; query: any;
comicObjectId: any; comicObjectId: any;
@@ -96,9 +97,12 @@ export const AcquisitionPanel = (
(searchInstanceId, resultId, name, size, type) => { (searchInstanceId, resultId, name, size, type) => {
dispatch( dispatch(
downloadAirDCPPItem( downloadAirDCPPItem(
searchInstanceId, resultId, searchInstanceId,
resultId,
props.comicObjectId, props.comicObjectId,
name, size, type, name,
size,
type,
airDCPPConfiguration.airDCPPState.socket, airDCPPConfiguration.airDCPPState.socket,
{ {
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
@@ -163,7 +167,7 @@ export const AcquisitionPanel = (
} }
> >
<span className="icon is-small"> <span className="icon is-small">
<img src="/img/airdcpp_logo.svg" /> <img src="/src/client/assets/img/airdcpp_logo.svg" />
</span> </span>
<span className="airdcpp-text">Search on AirDC++</span> <span className="airdcpp-text">Search on AirDC++</span>
</button> </button>
@@ -332,4 +336,4 @@ export const AcquisitionPanel = (
); );
}; };
export default AcquisitionPanel; export default AcquisitionPanel;

View File

@@ -66,7 +66,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
); );
const extractedComicBook = useSelector( const extractedComicBook = useSelector(
(state: RootState) => state.fileOps.extractedComicBookArchive, (state: RootState) => state.fileOps.extractedComicBookArchive.reading,
); );
const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicObjectId } = useParams<{ comicObjectId: string }>();
@@ -77,7 +77,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
dispatch( dispatch(
extractComicArchive(filePath, { extractComicArchive(filePath, {
type: "full", type: "full",
purpose: "readComicBook", purpose: "reading",
imageResizeOptions: { imageResizeOptions: {
baseWidth: 1024, baseWidth: 1024,
}, },
@@ -317,4 +317,4 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
); );
}; };
export default ComicDetail; export default ComicDetail;

View File

@@ -1,7 +1,5 @@
import React, { useEffect, useContext, ReactElement } from "react"; import React, { useEffect, useContext, ReactElement } from "react";
import { import { getBundlesForComic } from "../../actions/airdcpp.actions";
getBundlesForComic,
} from "../../actions/airdcpp.actions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "threetwo-ui-typings"; import { RootState } from "threetwo-ui-typings";
import { isEmpty, isNil, map } from "lodash"; import { isEmpty, isNil, map } from "lodash";

View File

@@ -1,7 +1,7 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { isUndefined } from "lodash"; import { isEmpty } from "lodash";
export const RawFileDetails = (props): ReactElement => { export const RawFileDetails = (props): ReactElement => {
const { rawFileDetails, inferredMetadata } = props.data; const { rawFileDetails, inferredMetadata } = props.data;
@@ -34,11 +34,20 @@ export const RawFileDetails = (props): ReactElement => {
</span> </span>
</div> </div>
</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> </div>
</dd> </dd>
</dl> </dl>
</div> </div>
<div className="content comic-detail raw-file-details mt-3 column is-one-third">
<div className="content comic-detail raw-file-details mt-3 column is-three-fifths">
<dl> <dl>
{/* inferred metadata */} {/* inferred metadata */}
<dt>Inferred Issue Metadata</dt> <dt>Inferred Issue Metadata</dt>
@@ -52,7 +61,7 @@ export const RawFileDetails = (props): ReactElement => {
</span> </span>
</div> </div>
</div> </div>
{!isUndefined(inferredMetadata.issue.number) ? ( {!isEmpty(inferredMetadata.issue.number) ? (
<div className="control"> <div className="control">
<div className="tags has-addons"> <div className="tags has-addons">
<span className="tag">Number</span> <span className="tag">Number</span>
@@ -80,6 +89,7 @@ RawFileDetails.propTypes = {
fileSize: PropTypes.number, fileSize: PropTypes.number,
path: PropTypes.string, path: PropTypes.string,
extension: PropTypes.string, extension: PropTypes.string,
mimeType: PropTypes.string,
cover: PropTypes.shape({ cover: PropTypes.shape({
filePath: PropTypes.string, filePath: PropTypes.string,
}), }),
@@ -93,4 +103,4 @@ RawFileDetails.propTypes = {
}), }),
}), }),
}), }),
}; };

View File

@@ -14,7 +14,7 @@ export const ArchiveOperations = (props): ReactElement => {
(state: RootState) => state.fileOps.comicBookExtractionInProgress, (state: RootState) => state.fileOps.comicBookExtractionInProgress,
); );
const extractedComicBookArchive = useSelector( const extractedComicBookArchive = useSelector(
(state: RootState) => state.fileOps.extractedComicBookArchive, (state: RootState) => state.fileOps.extractedComicBookArchive.analysis,
); );
const imageAnalysisResult = useSelector((state: RootState) => { const imageAnalysisResult = useSelector((state: RootState) => {
@@ -23,7 +23,15 @@ export const ArchiveOperations = (props): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const unpackComicArchive = useCallback(() => { const unpackComicArchive = useCallback(() => {
dispatch(extractComicArchive(data.rawFileDetails.filePath)); dispatch(
extractComicArchive(data.rawFileDetails.filePath, {
type: "full",
purpose: "analysis",
imageResizeOptions: {
baseWidth: 275,
},
}),
);
}, []); }, []);
// sliding panel config // sliding panel config
@@ -118,4 +126,4 @@ export const ArchiveOperations = (props): ReactElement => {
); );
}; };
export default ArchiveOperations; export default ArchiveOperations;

View File

@@ -11,7 +11,7 @@ import {
getComicBooks, getComicBooks,
} from "../../actions/fileops.actions"; } from "../../actions/fileops.actions";
import { getLibraryStatistics } from "../../actions/comicinfo.actions"; import { getLibraryStatistics } from "../../actions/comicinfo.actions";
import { isEmpty } from "lodash"; import { isEmpty, isNil } from "lodash";
export const Dashboard = (): ReactElement => { export const Dashboard = (): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -43,7 +43,7 @@ export const Dashboard = (): ReactElement => {
}, []); }, []);
const recentComics = useSelector( const recentComics = useSelector(
(state: RootState) => state.fileOps.recentComics, (state: RootState) => state.fileOps.recentComics
); );
const wantedComics = useSelector( const wantedComics = useSelector(
(state: RootState) => state.fileOps.wantedComics, (state: RootState) => state.fileOps.wantedComics,
@@ -60,7 +60,7 @@ export const Dashboard = (): ReactElement => {
<section className="section"> <section className="section">
<h1 className="title">Dashboard</h1> <h1 className="title">Dashboard</h1>
{!isEmpty(recentComics) && !isEmpty(recentComics.docs) ? ( {!isEmpty(recentComics) ? (
<> <>
{/* Pull List */} {/* Pull List */}
<PullList issues={recentComics} /> <PullList issues={recentComics} />
@@ -74,9 +74,8 @@ export const Dashboard = (): ReactElement => {
<WantedComicsList comics={wantedComics} /> <WantedComicsList comics={wantedComics} />
)} )}
{/* Recent imports */} {/* Recent imports */}
{!isEmpty(recentComics) && (
<RecentlyImported comicBookCovers={recentComics} /> <RecentlyImported comicBookCovers={recentComics} />
)}
{/* Volumes */} {/* Volumes */}
{!isEmpty(volumeGroups) && ( {!isEmpty(volumeGroups) && (
<VolumeGroups volumeGroups={volumeGroups} /> <VolumeGroups volumeGroups={volumeGroups} />
@@ -95,4 +94,4 @@ export const Dashboard = (): ReactElement => {
); );
}; };
export default Dashboard; export default Dashboard;

View File

@@ -20,7 +20,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2022-11-15", startDate: "2023-3-25",
pageSize: "15", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),
@@ -161,4 +161,4 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
); );
}; };
export default PullList; export default PullList;

View File

@@ -2,7 +2,6 @@ import React, { ReactElement } from "react";
import Card from "../Carda"; import Card from "../Carda";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { escapePoundSymbol } from "../../shared/utils/formatting.utils";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map } from "lodash";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
@@ -24,7 +23,6 @@ export const RecentlyImported = ({
700: 2, 700: 2,
600: 2, 600: 2,
}; };
return ( return (
<> <>
<div className="content mt-5"> <div className="content mt-5">
@@ -41,7 +39,7 @@ export const RecentlyImported = ({
columnClassName="recent-comics-column" columnClassName="recent-comics-column"
> >
{map( {map(
comicBookCovers.docs, comicBookCovers,
( (
{ {
_id, _id,
@@ -64,7 +62,6 @@ export const RecentlyImported = ({
comicInfo, comicInfo,
locg, locg,
}); });
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine) &&
!isUndefined(comicvine.volumeInformation); !isUndefined(comicvine.volumeInformation);
@@ -86,14 +83,14 @@ export const RecentlyImported = ({
{/* Raw file presence */} {/* Raw file presence */}
{isNil(rawFileDetails) && ( {isNil(rawFileDetails) && (
<span className="icon custom-icon is-small has-text-danger mr-2"> <span className="icon custom-icon is-small has-text-danger mr-2">
<img src="/img/missing-file.svg" /> <img src="/src/client/assets/img/missing-file.svg" />
</span> </span>
)} )}
{/* ComicInfo.xml presence */} {/* ComicInfo.xml presence */}
{!isNil(comicInfo) && !isEmpty(comicInfo) && ( {!isNil(comicInfo) && !isEmpty(comicInfo) && (
<span className="icon custom-icon is-small has-text-danger"> <span className="icon custom-icon is-small has-text-danger">
<img <img
src="/img/comicinfoxml.svg" src="/src/client/assets/img/comicinfoxml.svg"
alt={"ComicInfo.xml file detected."} alt={"ComicInfo.xml file detected."}
/> />
</span> </span>
@@ -102,8 +99,8 @@ export const RecentlyImported = ({
{isComicBookMetadataAvailable && ( {isComicBookMetadataAvailable && (
<span className="icon custom-icon"> <span className="icon custom-icon">
<img <img
src="/img/cvlogo.svg" src="/src/client/assets/img/cvlogo.svg"
alt={"ComicInfo.xml file detected."} alt={"ComicVine metadata detected."}
/> />
</span> </span>
)} )}
@@ -123,7 +120,7 @@ export const RecentlyImported = ({
</div> </div>
</Card> </Card>
{/* metadata card */} {/* metadata card */}
{!isNil(name) ? ( {!isNil(name) && (
<Card orientation="horizontal" hasDetails imageUrl={coverURL}> <Card orientation="horizontal" hasDetails imageUrl={coverURL}>
<dd className="is-size-9"> <dd className="is-size-9">
<dl> <dl>
@@ -138,7 +135,7 @@ export const RecentlyImported = ({
</dl> </dl>
</dd> </dd>
</Card> </Card>
) : null} )}
</React.Fragment> </React.Fragment>
); );
}, },
@@ -146,4 +143,4 @@ export const RecentlyImported = ({
</Masonry> </Masonry>
</> </>
); );
}; };

View File

@@ -82,12 +82,12 @@ export const WantedComicsList = ({
{/* comicVine metadata presence */} {/* comicVine metadata presence */}
{isComicBookMetadataAvailable && ( {isComicBookMetadataAvailable && (
<span className="icon custom-icon"> <span className="icon custom-icon">
<img src="/img/cvlogo.svg" /> <img src="/src/client/assets/img/cvlogo.svg" />
</span> </span>
)} )}
{!isEmpty(locg) && ( {!isEmpty(locg) && (
<span className="icon custom-icon"> <span className="icon custom-icon">
<img src="/img/locglogo.svg" /> <img src="/src/client/assets/img/locglogo.svg" />
</span> </span>
)} )}
{/* Issue type */} {/* Issue type */}

View File

@@ -3,7 +3,6 @@ import { getTransfers } from "../../actions/airdcpp.actions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
import { searchIssue } from "../../actions/fileops.actions";
import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { determineCoverFile } from "../../shared/utils/metadata.utils";
import MetadataPanel from "../shared/MetadataPanel"; import MetadataPanel from "../shared/MetadataPanel";
@@ -47,7 +46,6 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
}) })
setBundles(foo); setBundles(foo);
} }
}, [issueBundles]) }, [issueBundles])
return !isNil(bundles) ? return !isNil(bundles) ?
@@ -56,9 +54,9 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
<h1 className="title">Downloads</h1> <h1 className="title">Downloads</h1>
<div className="columns"> <div className="columns">
<div className="column is-half"> <div className="column is-half">
{bundles.map(bundle => { {bundles.map((bundle, idx) => {
console.log(bundle); console.log(bundle);
return <> return <div key={idx}>
<MetadataPanel <MetadataPanel
data={bundle} data={bundle}
imageStyle={{ maxWidth: 80 }} imageStyle={{ maxWidth: 80 }}
@@ -70,30 +68,33 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
margin: "0 0 8px 0", margin: "0 0 8px 0",
}} /> }} />
<table className="table is-size-7"> <table className="table is-size-7">
<thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Size</th> <th>Size</th>
<th>Type</th> <th>Type</th>
<th>Bundle ID</th> <th>Bundle ID</th>
</tr> </tr>
</thead>
{bundle.acquisition.directconnect.downloads.map((bundle) => { <tbody>
return(<tr> {bundle.acquisition.directconnect.downloads.map((bundle, idx) => {
return (<tr key={idx}>
<td>{bundle.name}</td> <td>{bundle.name}</td>
<td>{bundle.size}</td> <td>{bundle.size}</td>
<td>{bundle.type.str}</td> <td>{bundle.type.str}</td>
<td><span className="tag is-warning">{bundle.bundleId}</span></td> <td><span className="tag is-warning">{bundle.bundleId}</span></td>
</tr>) </tr>)
})} })}
</table> </tbody>
</table>
{/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */} {/* <pre>{JSON.stringify(bundle.acquisition.directconnect.downloads, null, 2)}</pre> */}
</> </div>
})} })}
</div> </div>
</div> </div>
</section> </section>
</div> : <div>asd</div>; </div> : <div>There are no downloads.</div>;
}; };
export default Downloads; export default Downloads;

View File

@@ -31,7 +31,7 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
from: 0, from: 0,
}, },
type: "volumeName", type: "volumeName",
trigger: "globalSearchBar" trigger: "globalSearchBar",
}, },
), ),
); );
@@ -51,7 +51,6 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
<i className="fa-solid fa-magnifying-glass"></i> <i className="fa-solid fa-magnifying-glass"></i>
</span> </span>
</div> </div>
{!isEmpty(searchResults) ? ( {!isEmpty(searchResults) ? (
<div <div
className="columns box is-multiline" className="columns box is-multiline"
@@ -62,9 +61,10 @@ export const SearchBar = (data: ISearchBarProps): ReactElement => {
margin: "60px 0 0 350px", margin: "60px 0 0 350px",
}} }}
> >
{map(searchResults, (result) => ( {map(searchResults, (result, idx) => (
<MetadataPanel <MetadataPanel
data={result} data={result}
key={idx}
imageStyle={{ maxWidth: 70 }} imageStyle={{ maxWidth: 70 }}
titleStyle={{ fontSize: "0.8rem" }} titleStyle={{ fontSize: "0.8rem" }}
tagsStyle={{ fontSize: "0.7rem" }} tagsStyle={{ fontSize: "0.7rem" }}

View File

@@ -8,10 +8,9 @@ import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
/** /**
* Component that tabulates the contents of the user's ThreeTwo Library. * Component that tabulates the contents of the user's ThreeTwo Library.
* *
* @component * @component
* @example * @example
* <Library /> * <Library />
@@ -20,9 +19,7 @@ export const Library = (): ReactElement => {
const searchResults = useSelector( const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics, (state: RootState) => state.fileOps.libraryComics,
); );
const searchError = useSelector( const searchError = useSelector((state: RootState) => state.fileOps.librarySearchError);
(state: RootState) => state.fileOps.librarySearchError,
);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
dispatch( dispatch(
@@ -36,7 +33,7 @@ export const Library = (): ReactElement => {
from: 0, from: 0,
}, },
type: "all", type: "all",
trigger: "libraryPage" trigger: "libraryPage",
}, },
), ),
); );
@@ -89,63 +86,67 @@ export const Library = (): ReactElement => {
const WantedStatus = ({ value }) => { const WantedStatus = ({ value }) => {
return !value ? <span className="tag is-info is-light">Wanted</span> : null; return !value ? <span className="tag is-info is-light">Wanted</span> : null;
}; };
const columns = useMemo(() => [ const columns = useMemo(
{ () => [
header: "Comic Metadata", {
footer: 1, header: "Comic Metadata",
columns: [ footer: 1,
{ columns: [
header: "File Details", {
id: "fileDetails", header: "File Details",
minWidth: 400, id: "fileDetails",
accessorKey: "_source", minWidth: 400,
cell: info => { accessorKey: "_source",
return <MetadataPanel data={info.getValue()} />; cell: (info) => {
return <MetadataPanel data={info.getValue()} />;
},
}, },
}, {
{ header: "ComicInfo.xml",
header: "ComicInfo.xml", accessorKey: "_source.sourcedMetadata.comicInfo",
accessorKey: "_source.sourcedMetadata.comicInfo", align: "center",
align: "center", minWidth: 250,
minWidth: 250, cell: (info) =>
cell: info => !isEmpty(info.getValue()) ? (
!isEmpty(info.getValue()) ? ( <ComicInfoXML data={info.getValue()} />
<ComicInfoXML data={info.getValue()} /> ) : (
) : ( <span className="tag mt-5">No ComicInfo.xml</span>
<span className="tag mt-5">No ComicInfo.xml</span> ),
),
},
],
},
{
header: "Additional Metadata",
columns: [
{
header: "Publisher",
accessorKey:
"_source.sourcedMetadata.comicvine.volumeInformation",
cell: info => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{info.getValue().publisher.name}
</h6>
)
);
}, },
}, ],
{ },
header: "Something", {
accessorKey: "_source.acquisition.source.wanted", header: "Additional Metadata",
cell: info => { columns: [
!isUndefined(info.getValue()) ? {
<WantedStatus value={info.getValue().toString()} /> : "Nothing"; header: "Publisher",
accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation",
cell: (info) => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{info.getValue().publisher.name}
</h6>
)
);
},
}, },
}, {
], header: "Something",
} accessorKey: "_source.acquisition.source.wanted",
], []); cell: (info) => {
!isUndefined(info.getValue()) ? (
<WantedStatus value={info.getValue().toString()} />
) : (
"Nothing"
);
},
},
],
},
],
[],
);
/** /**
* Pagination control that fetches the next x (pageSize) items * Pagination control that fetches the next x (pageSize) items
@@ -153,7 +154,7 @@ export const Library = (): ReactElement => {
* @param {number} pageIndex * @param {number} pageIndex
* @param {number} pageSize * @param {number} pageSize
* @returns void * @returns void
* *
**/ **/
const nextPage = useCallback((pageIndex: number, pageSize: number) => { const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch( dispatch(
@@ -173,7 +174,6 @@ export const Library = (): ReactElement => {
); );
}, []); }, []);
/** /**
* Pagination control that fetches the previous x (pageSize) items * Pagination control that fetches the previous x (pageSize) items
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index * based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
@@ -199,7 +199,7 @@ export const Library = (): ReactElement => {
from, from,
}, },
type: "all", type: "all",
trigger: "libraryPage" trigger: "libraryPage",
}, },
), ),
); );
@@ -208,20 +208,19 @@ export const Library = (): ReactElement => {
// ImportStatus.propTypes = { // ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired, // value: PropTypes.bool.isRequired,
// }; // };
return ( return (
<section className="container"> <section className="container">
<div className="section"> <div className="section">
<div className="header-area"> <div className="header-area">
<h1 className="title">Library</h1> <h1 className="title">Library</h1>
</div> </div>
{!isUndefined(searchResults.hits) && ( {!isEmpty(searchResults) ? (
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table
totalPages={searchResults.hits.total.value} totalPages={searchResults.total.value}
columns={columns} columns={columns}
sourceData={searchResults?.hits?.hits} sourceData={searchResults?.hits}
rowClickHandler={navigateToComicDetail} rowClickHandler={navigateToComicDetail}
paginationHandlers={{ paginationHandlers={{
nextPage, nextPage,
@@ -230,6 +229,26 @@ export const Library = (): ReactElement => {
/> />
</div> </div>
</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>
)} )}
</div> </div>
</section> </section>

View File

@@ -1,21 +1,31 @@
import React from "react"; import React, { useContext } from "react";
import { SearchBar } from "./GlobalSearchBar/SearchBar"; import { SearchBar } from "./GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick"; import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { format, fromUnixTime } from "date-fns";
const Navbar: React.FunctionComponent = (props) => { const Navbar: React.FunctionComponent = (props) => {
const downloadProgressTick = useSelector( const downloadProgressTick = useSelector(
(state: RootState) => state.airdcpp.downloadProgressData, (state: RootState) => state.airdcpp.downloadProgressData,
); );
const airDCPPSocketConnectionStatus = useSelector(
(state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
);
const airDCPPSessionInfo = useSelector(
(state: RootState) => state.airdcpp.airDCPPSessionInfo,
);
const socketDisconnectionReason = useSelector(
(state: RootState) => state.airdcpp.socketDisconnectionReason,
);
return ( return (
<nav className="navbar is-fixed-top"> <nav className="navbar is-fixed-top">
<div className="navbar-brand"> <div className="navbar-brand">
<Link to="/" className="navbar-item"> <Link to="/" className="navbar-item">
<img <img
src="/img/threetwo.svg" src="/src/client/assets/img/threetwo.svg"
alt="ThreeTwo! A comic book curator" alt="ThreeTwo! A comic book curator"
width="112" width="112"
height="28" height="28"
@@ -72,18 +82,66 @@ const Navbar: React.FunctionComponent = (props) => {
<div className="navbar-item has-dropdown is-hoverable"> <div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless"> <a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i> <i className="fa-solid fa-download"></i>
{downloadProgressTick && ( {downloadProgressTick && <div className="pulsating-circle"></div>}
<div className="pulsating-circle"></div>
)}
</a> </a>
{!isUndefined(downloadProgressTick) ? ( {!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown download-progress-meter"> <div className="navbar-dropdown is-right">
<a className="navbar-item"> <a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} /> <DownloadProgressTick data={downloadProgressTick} />
</a> </a> </div>
</div>
) : null} ) : null}
</div> </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 mt-3 pt-4 pr-2 pl-2 is-right airdcpp-status is-boxed">
{/* AirDC++ Session Information */}
<p>
Last login was{" "}
<span className="tag">
{format(
fromUnixTime(airDCPPSessionInfo.user.last_login),
"dd MMMM, yyyy",
)}
</span>
</p>
<hr className="navbar-divider" />
<p>
<span className="tag has-text-success">
{airDCPPSessionInfo.user.username}
</span>
connected to{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.system_info.client_version}
</span>{" "}
with session ID{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.session_id}
</span>
</p>
{/* <pre>{JSON.stringify(airDCPPSessionInfo, null, 2)}</pre> */}
</div>
</>
) : (
<>
<a className="navbar-link is-arrowless has-text-danger">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right is-boxed">
<pre>
{JSON.stringify(socketDisconnectionReason, null, 2)}
</pre>
</div>
</>
)}
</div>
<div className="navbar-item has-dropdown is-hoverable is-mega"> <div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div> <div className="navbar-link flex">Blog</div>
<div id="blogDropdown" className="navbar-dropdown"> <div id="blogDropdown" className="navbar-dropdown">

View File

@@ -166,8 +166,8 @@ export const Volumes = (props): ReactElement => {
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table
sourceData={volumes?.hits?.hits} sourceData={volumes?.hits}
totalPages={volumes.hits.hits.length} totalPages={volumes.hits.length}
paginationHandlers={{ paginationHandlers={{
nextPage: () => {}, nextPage: () => {},
previousPage: () => {}, previousPage: () => {},

View File

@@ -73,8 +73,8 @@ export const WantedComics = (props): ReactElement => {
id: "downloadDetails", id: "downloadDetails",
accessorKey: "acquisition", accessorKey: "acquisition",
cell: data => <ol> cell: data => <ol>
{data.getValue().directconnect.downloads.map(download => { {data.getValue().directconnect.downloads.map((download, idx) => {
return <li className="is-size-7">{download.name}</li>; return <li className="is-size-7" key={idx}>{download.name}</li>;
})} })}
</ol> </ol>
}, },

View File

@@ -57,6 +57,12 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
> >
{rawFileDetails.extension} {rawFileDetails.extension}
</span> </span>
<span
className="tag is-success is-light has-text-weight-semibold"
style={props.tagsStyle}
>
{rawFileDetails.mimeType}
</span>
<span <span
className="tag is-success is-light" className="tag is-success is-light"
style={props.tagsStyle} style={props.tagsStyle}

View File

@@ -20,8 +20,6 @@ export const T2Table = (tableOptions): ReactElement => {
pageIndex: 1, pageIndex: 1,
pageSize: 15, pageSize: 15,
}); });
console.log(sourceData)
const pagination = useMemo( const pagination = useMemo(
() => ({ () => ({
@@ -81,12 +79,12 @@ export const T2Table = (tableOptions): ReactElement => {
{/* <p>{totalPages} comics in all</p> */} {/* <p>{totalPages} comics in all</p> */}
</div> </div>
<div className="field has-addons"> <div className="field has-addons">
<p className="control"> <div className="control">
<div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div> <div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div>
</p> </div>
<p className="control"> <div className="control">
<div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div> <div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div>
</p> </div>
<div className="field has-addons ml-5"> <div className="field has-addons ml-5">
<p className="control"> <p className="control">

View File

@@ -81,6 +81,8 @@ export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS =
export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED = export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED =
"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 // Image file stats
export const IMG_ANALYSIS_CALL_IN_PROGRESS = "IMG_ANALYSIS_CALL_IN_PROGRESS"; export const IMG_ANALYSIS_CALL_IN_PROGRESS = "IMG_ANALYSIS_CALL_IN_PROGRESS";
export const IMG_ANALYSIS_DATA_FETCH_SUCCESS = export const IMG_ANALYSIS_DATA_FETCH_SUCCESS =
@@ -113,6 +115,8 @@ export const AIRDCPP_FILE_DOWNLOAD_COMPLETED =
export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT"; export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED"; export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK"; export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK";
export const AIRDCPP_SOCKET_CONNECTED = "AIRDCPP_SOCKET_CONNECTED";
export const AIRDCPP_SOCKET_DISCONNECTED = "AIRDCPP_SOCKET_DISCONNECTED";
// Transfers // Transfers
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED"; export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
@@ -135,4 +139,4 @@ export const SETTINGS_DB_FLUSH_SUCCESS = "SETTINGS_DB_FLUSH_SUCCESS";
// Metron Metadata // Metron Metadata
export const METRON_DATA_FETCH_SUCCESS = "METRON_DATA_FETCH_SUCCESS"; export const METRON_DATA_FETCH_SUCCESS = "METRON_DATA_FETCH_SUCCESS";
export const METRON_DATA_FETCH_IN_PROGRESS = "METRON_DATA_FETCH_IN_PROGRESS"; export const METRON_DATA_FETCH_IN_PROGRESS = "METRON_DATA_FETCH_IN_PROGRESS";
export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR"; export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR";

View File

@@ -11,68 +11,68 @@ export const hostURIBuilder = (options: Record<string, string>): string => {
export const CORS_PROXY_SERVER_URI = hostURIBuilder({ export const CORS_PROXY_SERVER_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "8050", port: "8050",
apiPath: "/", apiPath: "/",
}); });
export const COMICVINE_SERVICE_URI = hostURIBuilder({ export const COMICVINE_SERVICE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3080", port: "3080",
apiPath: "/api/comicvine", apiPath: "/api/comicvine",
}); });
export const METRON_SERVICE_URI = hostURIBuilder({ export const METRON_SERVICE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3080", port: "3080",
apiPath: "/api/metron", apiPath: "/api/metron",
}); });
export const API_BASE_URI = hostURIBuilder({ export const API_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "8050", port: "8050",
apiPath: "/api", apiPath: "/api",
}); });
export const LIBRARY_SERVICE_HOST = hostURIBuilder({ export const LIBRARY_SERVICE_HOST = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: ``, apiPath: ``,
}); });
export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({ export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/library", apiPath: "/api/library",
}); });
export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({ export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/search", apiPath: "/api/search",
}); });
export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({ export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/settings", apiPath: "/api/settings",
}); });
export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({ export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/imagetransformation", apiPath: "/api/imagetransformation",
}); });
export const SOCKET_BASE_URI = hostURIBuilder({ export const SOCKET_BASE_URI = hostURIBuilder({
protocol: "ws", protocol: "ws",
host: process.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3001", port: "3001",
apiPath: `/`, apiPath: `/`,
}); });

View File

@@ -1,7 +1,9 @@
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import React, { createContext, useEffect, useState } from "react"; import React, { createContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { toggleAirDCPPSocketConnectionStatus } from "../actions/airdcpp.actions";
import { getSettings } from "../actions/settings.actions"; import { getSettings } from "../actions/settings.actions";
import AirDCPPSocket from "../services/DcppSearchService"; import AirDCPPSocket from "../services/DcppSearchService";
const AirDCPPSocketContextProvider = ({ children }) => { const AirDCPPSocketContextProvider = ({ children }) => {
@@ -12,7 +14,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
airDCPPState: { airDCPPState: {
settings: settingsObject, settings: settingsObject,
socket: {}, socket: {},
socketConnectionInformation: {}, socketConectionInformation: {},
}, },
}); });
}; };
@@ -51,16 +53,35 @@ const AirDCPPSocketContextProvider = ({ children }) => {
client: { host }, client: { host },
}, },
} = configuration; } = configuration;
const initializedAirDCPPSocket = new AirDCPPSocket({ const initializedAirDCPPSocket = new AirDCPPSocket({
protocol: `${host.protocol}`, protocol: `${host.protocol}`,
hostname: `${host.hostname}:${host.port}`, hostname: `${host.hostname}:${host.port}`,
username: `${host.username}`,
password: `${host.password}`,
}); });
const socketConnectionInformation = await initializedAirDCPPSocket.connect( // connect and disconnect handlers
`${host.username}`, initializedAirDCPPSocket.onConnected = (sessionInfo) => {
`${host.password}`, dispatch(toggleAirDCPPSocketConnectionStatus("connected", sessionInfo));
true, };
); 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({ persistSettings({
...airDCPPState, ...airDCPPState,
airDCPPState: { airDCPPState: {
@@ -80,7 +101,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
}; };
const AirDCPPSocketContext = createContext({ const AirDCPPSocketContext = createContext({
airDCPPState: {}, airDCPPState: {},
saveSettings: () => {}, saveSettings: () => { },
}); });
export { AirDCPPSocketContext, AirDCPPSocketContextProvider }; export { AirDCPPSocketContext, AirDCPPSocketContextProvider };

View File

@@ -9,6 +9,8 @@ import {
AIRDCPP_BUNDLES_FETCHED, AIRDCPP_BUNDLES_FETCHED,
AIRDCPP_TRANSFERS_FETCHED, AIRDCPP_TRANSFERS_FETCHED,
LIBRARY_ISSUE_BUNDLES, LIBRARY_ISSUE_BUNDLES,
AIRDCPP_SOCKET_CONNECTED,
AIRDCPP_SOCKET_DISCONNECTED,
} from "../constants/action-types"; } from "../constants/action-types";
import { LOCATION_CHANGE } from "redux-first-history"; import { LOCATION_CHANGE } from "redux-first-history";
import { isNil, isUndefined } from "lodash"; import { isNil, isUndefined } from "lodash";
@@ -24,6 +26,9 @@ const initialState = {
downloadFileStatus: {}, downloadFileStatus: {},
bundles: [], bundles: [],
transfers: [], transfers: [],
isAirDCPPSocketConnected: false,
airDCPPSessionInfo: {},
socketDisconnectionReason: {},
}; };
function airdcppReducer(state = initialState, action) { function airdcppReducer(state = initialState, action) {
@@ -95,8 +100,23 @@ function airdcppReducer(state = initialState, action) {
...state, ...state,
transfers: action.bundles, transfers: action.bundles,
}; };
case AIRDCPP_SOCKET_CONNECTED:
return {
...state,
isAirDCPPSocketConnected: true,
airDCPPSessionInfo: action.data,
};
case AIRDCPP_SOCKET_DISCONNECTED:
return {
...state,
isAirDCPPSocketConnected: false,
socketDisconnectionReason: action.data,
};
case LOCATION_CHANGE: case LOCATION_CHANGE:
return { return {
...state,
searchResults: [], searchResults: [],
isAirDCPPSearchInProgress: false, isAirDCPPSearchInProgress: false,
searchInfo: null, searchInfo: null,
@@ -105,7 +125,6 @@ function airdcppReducer(state = initialState, action) {
bundleDBImportResult: null, bundleDBImportResult: null,
// bundles: [], // bundles: [],
}; };
default: default:
return state; return state;
} }

View File

@@ -29,7 +29,11 @@ import {
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
VOLUMES_FETCHED, VOLUMES_FETCHED,
COMICBOOK_EXTRACTION_SUCCESS,
} from "../constants/action-types"; } from "../constants/action-types";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
const initialState = { const initialState = {
IMSCallInProgress: false, IMSCallInProgress: false,
IMGCallInProgress: false, IMGCallInProgress: false,
@@ -42,7 +46,10 @@ const initialState = {
isComicVineMetadataImportInProgress: false, isComicVineMetadataImportInProgress: false,
comicVineMetadataImportError: {}, comicVineMetadataImportError: {},
rawImportError: {}, rawImportError: {},
extractedComicBookArchive: [], extractedComicBookArchive: {
reading: [],
analysis: [],
},
recentComics: [], recentComics: [],
wantedComics: [], wantedComics: [],
libraryComics: [], libraryComics: [],
@@ -81,7 +88,7 @@ function fileOpsReducer(state = initialState, action) {
case IMS_RECENT_COMICS_FETCHED: case IMS_RECENT_COMICS_FETCHED:
return { return {
...state, ...state,
recentComics: action.data, recentComics: action.data.docs,
}; };
case IMS_WANTED_COMICS_FETCHED: case IMS_WANTED_COMICS_FETCHED:
return { return {
@@ -131,13 +138,7 @@ function fileOpsReducer(state = initialState, action) {
comicBookExtractionInProgress: true, comicBookExtractionInProgress: true,
}; };
} }
case IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS: {
return {
...state,
extractedComicBookArchive: action.extractedComicBookArchive,
comicBookExtractionInProgress: false,
};
}
case LOCATION_CHANGE: { case LOCATION_CHANGE: {
return { return {
...state, ...state,
@@ -152,11 +153,44 @@ function fileOpsReducer(state = initialState, action) {
} }
case LS_COVER_EXTRACTED: { case LS_COVER_EXTRACTED: {
console.log("BASH", action); console.log("BASH", action);
if(state.recentComics.length === 5) {
state.recentComics.pop();
}
return { return {
...state, ...state,
librarySearchResultCount: state.librarySearchResultCount + 1, 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: { case LS_QUEUE_DRAINED: {
console.log("drained", action); console.log("drained", action);
return { return {
@@ -199,7 +233,7 @@ function fileOpsReducer(state = initialState, action) {
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: { case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
const foo = []; const foo = [];
if (!isUndefined(action.data.hits)) { if (!isUndefined(action.data.hits)) {
map(action.data.hits.hits, ({ _source }) => { map(action.data.hits, ({ _source }) => {
foo.push(_source); foo.push(_source);
}); });
} }
@@ -212,7 +246,7 @@ function fileOpsReducer(state = initialState, action) {
case WANTED_COMICS_FETCHED: { case WANTED_COMICS_FETCHED: {
const foo = []; const foo = [];
if (!isUndefined(action.data.hits)) { if (!isUndefined(action.data.hits)) {
map(action.data.hits.hits, ({ _source }) => { map(action.data.hits, ({ _source }) => {
foo.push(_source); foo.push(_source);
}); });
} }
@@ -229,7 +263,7 @@ function fileOpsReducer(state = initialState, action) {
volumes: action.data, volumes: action.data,
SSCallInProgress: false, SSCallInProgress: false,
}; };
case SS_SEARCH_FAILED: { case SS_SEARCH_FAILED: {
return { return {
...state, ...state,
@@ -249,4 +283,4 @@ function fileOpsReducer(state = initialState, action) {
} }
} }
export default fileOpsReducer; export default fileOpsReducer;

View File

@@ -10,7 +10,7 @@ class AirDCPPSocket {
} }
const options = { const options = {
url: `${socketProtocol}://${configuration.hostname}/api/v1/`, url: `${socketProtocol}://${configuration.hostname}/api/v1/`,
autoReconnect: false, autoReconnect: true,
reconnectInterval: 5, reconnectInterval: 5,
logLevel: "verbose", logLevel: "verbose",
ignoredListenerEvents: [ ignoredListenerEvents: [
@@ -18,6 +18,8 @@ class AirDCPPSocket {
"hash_statistics", "hash_statistics",
"hub_counts_updated", "hub_counts_updated",
], ],
username: `${configuration.username}`,
password: `${configuration.password}`,
}; };
const AirDCPPSocketInstance = Socket(options, window.WebSocket as any); const AirDCPPSocketInstance = Socket(options, window.WebSocket as any);
return AirDCPPSocketInstance; return AirDCPPSocketInstance;

View File

@@ -18,7 +18,7 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
} }
export const search = async (data: SearchData) => { export const search = async (data: SearchData) => {
await SocketService.connect("admin", "password"); await SocketService.connect();
const instance: SearchInstance = await SocketService.post("search"); const instance: SearchInstance = await SocketService.post("search");
const unsubscribe = await SocketService.addListener( const unsubscribe = await SocketService.addListener(
"search", "search",

View File

@@ -67,7 +67,10 @@ export const determineCoverFile = (data) => {
} }
}; };
export const determineExternalMetadata = (metadataSource, source) => { export const determineExternalMetadata = (
metadataSource: string,
source: any
) => {
switch (metadataSource) { switch (metadataSource) {
case "comicvine": case "comicvine":
return { return {
@@ -87,4 +90,4 @@ export const determineExternalMetadata = (metadataSource, source) => {
default: default:
break; break;
} }
}; };

View File

@@ -30,20 +30,20 @@ RawFileDetails.args = {
}, },
"inferredMetadata": { "inferredMetadata": {
"issue": { "issue": {
"name": "Fantastic Four 143", "name": "Batman Annual 02",
"number": 197402, "number": 197402,
"year": "1974", "year": "1974",
"subtitle": "" "subtitle": ""
} }
}, },
"rawFileDetails": { "rawFileDetails": {
"name": "197402 Fantastic Four 143", "name": "Batman Annual 02",
"filePath": "/Users/rishi/work/threetwo-core-service/comics/197402 Fantastic Four 143.cbz", "filePath": "/Users/rishi/work/threetwo-core-service/comics/197402 Fantastic Four 143.cbz",
"fileSize": 10341074, "fileSize": 10341074,
"extension": ".cbz", "extension": ".cbz",
"containedIn": "./userdata/covers/197402 Fantastic Four 143", "containedIn": "./userdata/covers/197402 Fantastic Four 143",
"cover": { "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": [] } }, "sourcedMetadata": { "comicInfo": null, "comicvine": { "aliases": [] } },

View File

@@ -30,12 +30,12 @@ app.use(bodyParser.urlencoded({ extended: true }));
// REGISTER ROUTES // REGISTER ROUTES
// all of the routes will be prefixed with /api // all of the routes will be prefixed with /api
const routes: Router[] = Object.values(router); const routes: Router[] = Object.values(router);
app.use("/api", routes); // app.use("/api", routes);
// Send index.html on root request // Send index.html on root request
app.use(express.static("dist")); // app.use(express.static("dist"));
app.use(express.static("public")); // 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}`);

View File

@@ -2,19 +2,21 @@
"extends": "@tsconfig/node14/tsconfig.json", "extends": "@tsconfig/node14/tsconfig.json",
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true, "target": "ESNext",
"noImplicitAny": false, "useDefineForClassFields": true,
"target": "es2019", "lib": ["DOM", "DOM.Iterable", "ESNext"],
"jsx": "react", "allowJs": false,
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist/",
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"lib": ["dom", "dom.iterable", "esnext", "webworker"], "isolatedModules": true,
"baseUrl": ".", "noEmit": true,
"esModuleInterop": true, "jsx": "react-jsx"
"strict": true
}, },
"settings": { "settings": {
"eslint.workingDirectories": [ "eslint.workingDirectories": [

22
vite.config.js Normal file
View File

@@ -0,0 +1,22 @@
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",
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"],
},
}),
],
});

View File

@@ -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),
};
};

11574
yarn.lock

File diff suppressed because it is too large Load Diff