Compare commits

..

59 Commits

Author SHA1 Message Date
dependabot[bot]
1bd3d611e4 Bump postcss from 8.4.29 to 8.4.31 (#93) 2023-10-19 08:37:49 -05:00
dependabot[bot]
825782fe13 Bump @babel/traverse from 7.22.11 to 7.23.2 (#94) 2023-10-19 08:37:41 -05:00
32f4055daa Import queue progress (#87)
* 🚥 Added service status panel scaffold

* 🐂 Support for showing import progress

* 🐂 Support for session-tracking

* 🔧 Tooling for resumable socket.io sessions

* 🧹 Minor change in socket.io connection code

* 🔧 Refactoring the Import Page

* 📝 Added more details to import statuses

* 🐂 Queue pause/resume functionality

* 🐂 Queue drain event reducer

* 🐂 Queue controls

* 🔧 Hardening the import UX

* 🔀 Bumped deps

* 🔧 Fixed the airdcpp-apisocket version

* ⛑️ Removed useless deps

* 🪡 Fixed margin on the comicinfo.xml panel on the library page

* 🏗️ Scaffold for job results

* 🔢 Removed the useless LS_IMPORT event

* 🔧 Wired up jobStatistics call

* 🧹 Cleaning up the tabulated job results

* 🔧 More finishing touches to Import UX

* 🔧 Added a console log for debugging purposes

---------

Co-authored-by: Rishi Ghan <hghan@apple.com>
2023-08-30 13:49:58 -04:00
dependabot[bot]
c20f24b1a2 Bump word-wrap from 1.2.3 to 1.2.4 (#88)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-11 14:11:53 -04:00
dependabot[bot]
e4fc28f698 Bump tough-cookie from 4.0.0 to 4.1.3 (#85)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.0.0 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.0.0...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 10:52:32 -07:00
dependabot[bot]
d6c183339f Bump semver from 5.7.1 to 5.7.2 (#86)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 10:52:20 -07:00
dependabot[bot]
0eba47e20f Bump vite from 4.2.0 to 4.2.3 (#82) 2023-06-05 22:37:43 -04:00
928bfd573e 🔧 Fix for the hostname env var when running in Docker (#81) 2023-05-16 12:41:57 -04:00
b8fb179ac6 🔧 Miscellaneous Fixes (#79)
* ️ Config key to expose hostname in a Docker context

* 🔧 Miscellaneous Fixes
2023-05-15 16:16:26 -04:00
94692bb6d4 71 dockerfile update (#75)
* 🐳 Fixes to the Dockerfile

* 🚢 Updated port number for UI

*  Removed the param serialization from axios calls
2023-05-15 16:13:40 -04:00
4b795aca5d ️ Config key to expose hostname in a Docker context (#77) 2023-04-27 11:38:50 -04:00
fbf6bed4fe 🐳 Fixes to the Dockerfile (#74) 2023-03-31 15:09:23 -04:00
Lars Gohr
18b18c3d81 Updated elgohr/Publish-Docker-Github-Action to a supported version (v5) (#73) 2023-03-30 21:00:12 -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
dependabot[bot]
f854ff9cc6 Bump decode-uri-component from 0.2.0 to 0.2.2 (#45)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  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>
2022-12-06 20:35:38 -08:00
815444a973 🔧 Refactoring the uncompression methods for "Read Comic" and "Uncompress Archive" operations (#43)
* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README
2022-12-06 14:08:53 -08:00
1cbf53be98 🖼️ Adding screenshots as of December 2022 (#44) 2022-12-06 14:07:44 -08:00
0e73f9dad1 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-11-24 22:00:25 -08:00
a15168a6be 📝 Added JSDoc to extractComicArchive method 2022-11-24 21:51:49 -08:00
dependabot[bot]
67079f0cb4 Bump engine.io from 6.2.0 to 6.2.1 (#42)
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  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>
2022-11-22 10:03:42 -08:00
ef5af01e33 ✏️ Code cleanup and adding jsDoc 2022-11-19 11:23:44 -08:00
5b6c9c8ffc ✏️ Added jsDoc for some components 2022-11-17 15:55:52 -08:00
4556778a47 🔧 Tweaking layout of the CV manual search form 2022-11-17 14:06:26 -08:00
af5f443cbe 🔧 Fixing CV manual search form 2022-11-17 13:46:20 -08:00
7babf9f73d 🔧 🏗️ Massive tables refactor
Abstracted a table component that can be configured to display issues, volumes or pull list items
2022-11-15 17:22:50 -08:00
1e39daeda2 🔧 Fixed wanted comics table data source 2022-11-08 10:05:30 -08:00
e3cea24615 🔧 Using the refactored T2Table for Wanted Comics 2022-11-07 21:15:02 -08:00
f60c9e4e67 🔧 Refactored the T2Table component with the new pagination controls 2022-11-07 20:55:58 -08:00
27e6f26331 ✏️ Changed the hard-coded date of the pull list 2022-11-07 08:06:09 -08:00
1f5502ce23 👁️ Added grid and tabular view buttons 2022-11-05 09:28:54 -07:00
3cb9588bbf 🎨 Styling tweaks to pagination controls 2022-11-03 21:40:23 -07:00
b1fb256189 🔧 Streamlined search and pagination controls on Library page 2022-11-03 12:47:31 -07:00
74ea2742f0 🔧 Fixes for controlled pagination on react-table 2022-11-01 22:21:58 -07:00
151c6ec314 🔎 Fixed search bar on Library page 2022-10-29 15:47:27 -07:00
c6a3be968a 🎨 Tweaking styling of the library table 2022-10-28 21:46:16 -07:00
ff5ce10e17 🔧 Library page table pagination 2022-10-27 23:06:40 -07:00
63e96bf96e 🚏 Added a path to comic detail row on Library page 2022-10-17 11:30:30 -07:00
b699a90a00 🕵🏼 Added manual search form for CV matching 2022-09-28 09:51:48 -07:00
eda11d3537 🐛 Fixed bug on the AirDC++ settings page 2022-09-19 15:41:32 -07:00
262f8d49d7 🔼 Bumped up react-final-form 2022-09-12 12:50:30 -07:00
6b1bb02d57 📝 Added a port number field to AirDC++ settings form 2022-09-08 10:35:30 -07:00
73 changed files with 7833 additions and 13204 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 = {
extends: ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:css-modules/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended"],
extends: ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:css-modules/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended", "plugin:storybook/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
ecmaFeatures: {
jsx: true // Allows for the parsing of JSX
}
},
plugins: ["@typescript-eslint", "css-modules"],
settings: {
"import/resolver": {
@@ -18,9 +18,9 @@ module.exports = {
},
react: {
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
}
},
// Fine tune rules
rules: {
"@typescript-eslint/no-var-requires": 0

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
uses: elgohr/Publish-Docker-Github-Action@v5
with:
name: frishi/threetwo
username: ${{ secrets.DOCKER_USERNAME }}

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",
},
};

20
.storybook/main.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
"@storybook/addon-mdx-gfm"
],
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;

View File

@@ -1,4 +1,4 @@
FROM node:17.3-alpine
FROM node:18.15.0-alpine
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
WORKDIR /threetwo
@@ -9,11 +9,11 @@ COPY nodemon.json ./
COPY jsdoc.json ./
# RUN apt-get update && apt-get install -y git python3 build-essential autoconf automake g++ libpng-dev make
RUN apk --no-cache add g++ make libpng-dev python3 git libc6-compat autoconf automake bash libjpeg-turbo-dev libpng-dev mesa-dev mesa libxi build-base gcc libtool nasm
RUN apk --no-cache add g++ make libpng-dev git python3 libc6-compat autoconf automake libjpeg-turbo-dev libpng-dev mesa-dev mesa libxi build-base gcc libtool nasm
RUN yarn --ignore-engines
COPY . .
EXPOSE 3050
EXPOSE 5173
ENTRYPOINT [ "npm", "start" ]

View File

@@ -4,6 +4,16 @@ ThreeTwo! _aims to be_ a comic book curation app.
[![Docker Image CI](https://github.com/rishighan/threetwo/actions/workflows/docker-image.yml/badge.svg)](https://github.com/rishighan/threetwo/actions/workflows/docker-image.yml)
### Screenshots
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/Dashboard.png)
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/Library.png)
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/DC%2B%2B%20integration.png)
![](https://github.com/rishighan/threetwo-visual-updates/raw/67e56878eb0381c73c1dea746a45253d3dcaa184/update_december_2022/ComicVine%20Matching.png)
### 🦄 Early Development Support Channel
@@ -30,9 +40,8 @@ For debugging and troubleshooting, you can run this app locally using these step
1. Clone this repo using `git clone https://github.com/rishighan/threetwo.git`
2. `yarn run dev` (you can ignore the warnings)
3. This will open `http://localhost:3050` in your default browser
4. For testing `OPDS` functionality, create a folder called `comics` under `/src/server` and put some comics in there. The `OPDS` feed is accessed to `http://localhost:8050/api/opds`
5. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
3. This will open `http://localhost:5173` in your default browser
4. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
## Troubleshooting

1
funding.yml Normal file
View File

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

View File

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

View File

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

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,
SearchResponse,
} from "threetwo-ui-typings";
import { LIBRARY_SERVICE_BASE_URI, SEARCH_SERVICE_BASE_URI } from "../constants/endpoints";
import {
LIBRARY_SERVICE_BASE_URI,
SEARCH_SERVICE_BASE_URI,
} from "../constants/endpoints";
import {
AIRDCPP_SEARCH_RESULTS_ADDED,
AIRDCPP_SEARCH_RESULTS_UPDATED,
@@ -18,6 +21,8 @@ import {
IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
AIRDCPP_TRANSFERS_FETCHED,
LIBRARY_ISSUE_BUNDLES,
AIRDCPP_SOCKET_CONNECTED,
AIRDCPP_SOCKET_DISCONNECTED,
} from "../constants/action-types";
import { isNil } from "lodash";
import axios from "axios";
@@ -32,190 +37,199 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const toggleAirDCPPSocketConnectionStatus =
(status: String, payload?: any) => async (dispatch) => {
switch (status) {
case "connected":
dispatch({
type: AIRDCPP_SOCKET_CONNECTED,
data: payload,
});
break;
case "disconnected":
dispatch({
type: AIRDCPP_SOCKET_DISCONNECTED,
data: payload,
});
break;
default:
console.log("Can't set AirDC++ socket status.");
break;
}
};
export const search =
(data: SearchData, ADCPPSocket: any, credentials: any) =>
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
credentials.username,
credentials.password,
true,
);
}
const instance: SearchInstance = await ADCPPSocket.post("search");
dispatch({
type: AIRDCPP_SEARCH_IN_PROGRESS,
});
// We want to get notified about every new result in order to make the user experience better
await ADCPPSocket.addListener(
`search`,
"search_result_added",
async (groupedResult) => {
// ...add the received result in the UI
// (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results)
dispatch({
type: AIRDCPP_SEARCH_RESULTS_ADDED,
groupedResult,
});
},
instance.id,
);
// We also want to update the existing items in our list when new hits arrive for the previously listed files/directories
await ADCPPSocket.addListener(
`search`,
"search_result_updated",
async (groupedResult) => {
// ...update properties of the existing result in the UI
dispatch({
type: AIRDCPP_SEARCH_RESULTS_UPDATED,
groupedResult,
});
},
instance.id,
);
// We need to show something to the user in case the search won't yield any results so that he won't be waiting forever)
// Wait for 5 seconds for any results to arrive after the searches were sent to the hubs
await ADCPPSocket.addListener(
`search`,
"search_hub_searches_sent",
async (searchInfo) => {
await sleep(5000);
// Check the number of received results (in real use cases we should know that even without calling the API)
const currentInstance = await ADCPPSocket.get(
`search/${instance.id}`,
);
if (currentInstance.result_count === 0) {
// ...nothing was received, show an informative message to the user
console.log("No more search results.");
}
// The search can now be considered to be "complete"
// If there's an "in progress" indicator in the UI, that could also be disabled here
dispatch({
type: AIRDCPP_HUB_SEARCHES_SENT,
searchInfo,
instance,
});
},
instance.id,
);
// Finally, perform the actual search
await ADCPPSocket.post(`search/${instance.id}/hub_search`, data);
} catch (error) {
console.log(error);
throw error;
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket();
}
};
const instance: SearchInstance = await ADCPPSocket.post("search");
dispatch({
type: AIRDCPP_SEARCH_IN_PROGRESS,
});
// We want to get notified about every new result in order to make the user experience better
await ADCPPSocket.addListener(
`search`,
"search_result_added",
async (groupedResult) => {
// ...add the received result in the UI
// (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results)
dispatch({
type: AIRDCPP_SEARCH_RESULTS_ADDED,
groupedResult,
});
},
instance.id,
);
// We also want to update the existing items in our list when new hits arrive for the previously listed files/directories
await ADCPPSocket.addListener(
`search`,
"search_result_updated",
async (groupedResult) => {
// ...update properties of the existing result in the UI
dispatch({
type: AIRDCPP_SEARCH_RESULTS_UPDATED,
groupedResult,
});
},
instance.id,
);
// We need to show something to the user in case the search won't yield any results so that he won't be waiting forever)
// Wait for 5 seconds for any results to arrive after the searches were sent to the hubs
await ADCPPSocket.addListener(
`search`,
"search_hub_searches_sent",
async (searchInfo) => {
await sleep(5000);
// Check the number of received results (in real use cases we should know that even without calling the API)
const currentInstance = await ADCPPSocket.get(
`search/${instance.id}`,
);
if (currentInstance.result_count === 0) {
// ...nothing was received, show an informative message to the user
console.log("No more search results.");
}
// The search can now be considered to be "complete"
// If there's an "in progress" indicator in the UI, that could also be disabled here
dispatch({
type: AIRDCPP_HUB_SEARCHES_SENT,
searchInfo,
instance,
});
},
instance.id,
);
// Finally, perform the actual search
await ADCPPSocket.post(`search/${instance.id}/hub_search`, data);
} catch (error) {
console.log(error);
throw error;
}
};
export const downloadAirDCPPItem =
(searchInstanceId: Number,
(
searchInstanceId: Number,
resultId: String,
comicObjectId: String,
name: String, size: Number, type: any,
name: String,
size: Number,
type: any,
ADCPPSocket: any,
credentials: any,
): void =>
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
`${credentials.username}`,
`${credentials.password}`,
true,
);
}
let bundleDBImportResult = {};
const downloadResult = await ADCPPSocket.post(
`search/${searchInstanceId}/results/${resultId}/download`,
);
if (!isNil(downloadResult)) {
bundleDBImportResult = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
bundleId: downloadResult.bundle_info.id,
comicObjectId,
name,
size,
type,
},
});
dispatch({
type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
downloadResult,
bundleDBImportResult,
});
dispatch({
type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
comicBookDetail: bundleDBImportResult.data,
IMS_inProgress: false,
});
}
} catch (error) {
throw error;
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect();
}
};
let bundleDBImportResult = {};
const downloadResult = await ADCPPSocket.post(
`search/${searchInstanceId}/results/${resultId}/download`,
);
export const getBundlesForComic =
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
`${credentials.username}`,
`${credentials.password}`,
true,
);
}
const comicObject = await axios({
if (!isNil(downloadResult)) {
bundleDBImportResult = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
id: `${comicObjectId}`,
bundleId: downloadResult.bundle_info.id,
comicObjectId,
name,
size,
type,
},
});
// get only the bundles applicable for the comic
if (comicObject.data.acquisition.directconnect) {
const filteredBundles = comicObject.data.acquisition.directconnect.downloads.map(
dispatch({
type: AIRDCPP_RESULT_DOWNLOAD_INITIATED,
downloadResult,
bundleDBImportResult,
});
dispatch({
type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
comicBookDetail: bundleDBImportResult.data,
IMS_inProgress: false,
});
}
} catch (error) {
throw error;
}
};
export const getBundlesForComic =
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect();
}
const comicObject = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
id: `${comicObjectId}`,
},
});
// get only the bundles applicable for the comic
if (comicObject.data.acquisition.directconnect) {
const filteredBundles =
comicObject.data.acquisition.directconnect.downloads.map(
async ({ bundleId }) => {
return await ADCPPSocket.get(`queue/bundles/${bundleId}`);
},
);
dispatch({
type: AIRDCPP_BUNDLES_FETCHED,
bundles: await Promise.all(filteredBundles),
});
}
} catch (error) {
throw error;
dispatch({
type: AIRDCPP_BUNDLES_FETCHED,
bundles: await Promise.all(filteredBundles),
});
}
};
} catch (error) {
throw error;
}
};
export const getTransfers =
(ADCPPSocket: any, credentials: any) => async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
`${credentials.username}`,
`${credentials.password}`,
true,
);
await ADCPPSocket.connect();
}
const bundles = await ADCPPSocket.get("queue/bundles/1/85", {});
if (!isNil(bundles)) {
@@ -234,7 +248,6 @@ export const getTransfers =
type: LIBRARY_ISSUE_BUNDLES,
issue_bundles,
});
}
} catch (err) {
throw err;

View File

@@ -1,10 +1,6 @@
import axios from "axios";
import rateLimiter from "axios-rate-limit";
import {
AxiosCacheRequestConfig,
createCacheAdapter,
} from "axios-simple-cache-adapter";
import qs from "qs";
import { setupCache } from "axios-cache-interceptor";
import {
CV_SEARCH_SUCCESS,
CV_API_CALL_IN_PROGRESS,
@@ -26,25 +22,21 @@ import {
LIBRARY_SERVICE_BASE_URI,
} from "../constants/endpoints";
const axiosCacheAdapter = createCacheAdapter();
const http = rateLimiter(axios.create(), {
maxRequests: 1,
perMilliseconds: 1000,
maxRPS: 1,
});
const cachedAxios = setupCache(axios);
export const getWeeklyPullList = (options) => async (dispatch) => {
try {
dispatch({
type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
});
await axios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
method: "get",
params: options,
axiosCacheAdapter,
cache: 1000, // value in MS
} as AxiosCacheRequestConfig).then((response) => {
}).then((response) => {
dispatch({
type: CV_WEEKLY_PULLLIST_FETCHED,
data: response.data.result,
@@ -70,9 +62,6 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: "repeat" });
},
});
switch (options.callURIAction) {
@@ -137,7 +126,7 @@ export const analyzeLibrary = (issues) => async (dispatch) => {
queryObjects,
},
});
dispatch({
type: CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED,
matches: foo.data,

View File

@@ -4,13 +4,12 @@ import {
COMICVINE_SERVICE_URI,
IMAGETRANSFORMATION_SERVICE_BASE_URI,
LIBRARY_SERVICE_BASE_URI,
LIBRARY_SERVICE_HOST,
SEARCH_SERVICE_BASE_URI,
JOB_QUEUE_SERVICE_BASE_URI,
} from "../constants/endpoints";
import {
IMS_COMIC_BOOK_GROUPS_FETCHED,
IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS,
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
IMS_RECENT_COMICS_FETCHED,
IMS_WANTED_COMICS_FETCHED,
CV_API_CALL_IN_PROGRESS,
@@ -22,21 +21,38 @@ import {
LS_IMPORT,
IMG_ANALYSIS_CALL_IN_PROGRESS,
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
SS_SEARCH_RESULTS_FETCHED,
SS_SEARCH_IN_PROGRESS,
FILEOPS_STATE_RESET,
LS_IMPORT_CALL_IN_PROGRESS,
LS_TOGGLE_IMPORT_QUEUE,
SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
WANTED_COMICS_FETCHED,
VOLUMES_FETCHED,
LIBRARY_SERVICE_HEALTH,
LS_SET_QUEUE_STATUS,
LS_IMPORT_JOB_STATISTICS_FETCHED,
} from "../constants/action-types";
import { success } from "react-notification-system-redux";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { isNil, map } from "lodash";
import { isNil } from "lodash";
export const getServiceStatus = (serviceName?: string) => async (dispatch) => {
axios
.request({
url: `${LIBRARY_SERVICE_BASE_URI}/getHealthInformation`,
method: "GET",
transformResponse: (r: string) => JSON.parse(r),
})
.then((response) => {
const { data } = response;
dispatch({
type: LIBRARY_SERVICE_HEALTH,
status: data,
});
});
};
export async function walkFolder(path: string): Promise<Array<IFolderData>> {
return axios
.request<Array<IFolderData>>({
@@ -58,15 +74,6 @@ export async function walkFolder(path: string): Promise<Array<IFolderData>> {
* @return the comic book metadata
*/
export const fetchComicBookMetadata = () => async (dispatch) => {
const extractionOptions = {
extractTarget: "cover",
targetExtractionFolder: "./userdata/covers",
extractionMode: "bulk",
paginationOptions: {
pageLimit: 25,
page: 1,
},
};
dispatch({
type: LS_IMPORT_CALL_IN_PROGRESS,
});
@@ -81,19 +88,38 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
// autoDismiss: 0,
// }),
// );
const sessionId = localStorage.getItem("sessionId");
dispatch({
type: LS_IMPORT,
meta: { remote: true },
data: { extractionOptions },
});
await axios.request({
url: `${LIBRARY_SERVICE_BASE_URI}/newImport`,
method: "POST",
data: { sessionId },
});
};
export const toggleImportQueueStatus = (options) => async (dispatch) => {
export const getImportJobResultStatistics = () => async (dispatch) => {
const result = await axios.request({
url: `${JOB_QUEUE_SERVICE_BASE_URI}/getJobResultStatistics`,
method: "GET",
});
dispatch({
type: LS_TOGGLE_IMPORT_QUEUE,
meta: { remote: true },
data: { manjhul: "jigyadam", action: options.action },
type: LS_IMPORT_JOB_STATISTICS_FETCHED,
data: result.data,
});
};
export const setQueueControl =
(queueAction: string, queueStatus: string) => async (dispatch) => {
dispatch({
type: LS_SET_QUEUE_STATUS,
meta: { remote: true },
data: { queueAction, queueStatus },
});
};
/**
* Fetches comic book metadata for various types
* @return metadata for the comic book object categories
@@ -134,46 +160,50 @@ export const getComicBooks = (options) => async (dispatch) => {
* @returns Nothing.
* @param payload
*/
export const importToDB = (sourceName: string, payload?: any) => (dispatch) => {
try {
const comicBookMetadata = {
rawFileDetails: {
name: "",
},
importStatus: {
isImported: true,
tagged: false,
matchedResult: {
score: "0",
export const importToDB =
(sourceName: string, metadata?: any) => (dispatch) => {
try {
const comicBookMetadata = {
importType: "new",
payload: {
rawFileDetails: {
name: "",
},
importStatus: {
isImported: true,
tagged: false,
matchedResult: {
score: "0",
},
},
sourcedMetadata: metadata || null,
acquisition: { source: { wanted: true, name: sourceName } },
},
},
sourcedMetadata: payload || null,
acquisition: { source: { wanted: true, name: sourceName } },
};
dispatch({
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
});
return axios
.request({
url: `${LIBRARY_SERVICE_BASE_URI}/rawImportToDb`,
method: "POST",
data: comicBookMetadata,
// transformResponse: (r: string) => JSON.parse(r),
})
.then((response) => {
const { data } = response;
dispatch({
type: IMS_CV_METADATA_IMPORT_SUCCESSFUL,
importResult: data,
});
};
dispatch({
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
});
} catch (error) {
dispatch({
type: IMS_CV_METADATA_IMPORT_FAILED,
importError: error,
});
}
};
return axios
.request({
url: `${LIBRARY_SERVICE_BASE_URI}/rawImportToDb`,
method: "POST",
data: comicBookMetadata,
// transformResponse: (r: string) => JSON.parse(r),
})
.then((response) => {
const { data } = response;
dispatch({
type: IMS_CV_METADATA_IMPORT_SUCCESSFUL,
importResult: data,
});
});
} catch (error) {
dispatch({
type: IMS_CV_METADATA_IMPORT_FAILED,
importError: error,
});
}
};
export const fetchVolumeGroups = () => async (dispatch) => {
try {
@@ -194,6 +224,7 @@ export const fetchVolumeGroups = () => async (dispatch) => {
};
export const fetchComicVineMatches =
(searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => {
console.log(issueSearchQuery);
try {
dispatch({
type: CV_API_CALL_IN_PROGRESS,
@@ -223,7 +254,6 @@ export const fetchComicVineMatches =
},
})
.then((response) => {
console.log(response);
let matches: any = [];
if (
!isNil(response.data.results) &&
@@ -233,7 +263,6 @@ export const fetchComicVineMatches =
} else {
matches = response.data.map((match) => match);
}
console.log(matches);
dispatch({
type: CV_SEARCH_SUCCESS,
searchResults: matches,
@@ -252,33 +281,37 @@ export const fetchComicVineMatches =
});
};
export const extractComicArchive = (path: string) => async (dispatch) => {
const comicBookPages: string[] = [];
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
});
const extractedComicBookArchive = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
filePath: path,
},
});
map(extractedComicBookArchive.data, (page) => {
console.log(page);
const pageFilePath = removeLeadingPeriod(page);
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
comicBookPages.push(imagePath);
});
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
extractedComicBookArchive: comicBookPages,
});
};
/**
* This method is a proxy to `uncompressFullArchive` which uncompresses complete `rar` or `zip` archives
* @param {string} path The path to the compressed archive
* @param {any} options Options object
* @returns {any}
*/
export const extractComicArchive =
(path: string, options: any): any =>
async (dispatch) => {
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
});
await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
filePath: path,
options,
},
});
};
/**
* Description
* @param {any} query
* @param {any} options
* @returns {any}
*/
export const searchIssue = (query, options) => async (dispatch) => {
dispatch({
type: SS_SEARCH_IN_PROGRESS,
@@ -301,28 +334,32 @@ export const searchIssue = (query, options) => async (dispatch) => {
case "wantedComicsPage":
dispatch({
type: WANTED_COMICS_FETCHED,
data: response.data.body,
data: response.data.hits,
});
break;
case "globalSearchBar":
dispatch({
type: SS_SEARCH_RESULTS_FETCHED_SPECIAL,
data: response.data.body,
data: response.data.hits,
});
break;
case "libraryPage":
dispatch({
type: SS_SEARCH_RESULTS_FETCHED,
data: response.data.body,
data: response.data.hits,
});
break;
case "volumesPage":
dispatch({
type: VOLUMES_FETCHED,
data: response.data.hits,
});
break;
default:
break;
}
};
export const analyzeImage =
(imageFilePath: string | Buffer) => async (dispatch) => {

View File

@@ -16,7 +16,6 @@ export const saveSettings =
method: "POST",
data: { settingsPayload, settingsObjectId },
});
console.log(result.data);
dispatch({
type: SETTINGS_OBJECT_FETCHED,
data: result.data,

View File

@@ -1,8 +1,8 @@
@import "../../../../node_modules/bulma/bulma.sass";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
@import "/node_modules/bulma/bulma.sass";
$fa-font-path: "/node_modules/@fortawesome/fontawesome-free/webfonts";
@import "/node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "/node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
@import "/node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
$bg-color: yellow;
$border-color: red;
@@ -62,6 +62,10 @@ pre {
margin-left: -300px;
min-width: 500px;
}
.airdcpp-status {
min-width: 300px;
line-height: 1.7rem;
}
body {
background: #454a59;
}
@@ -356,6 +360,10 @@ pre {
border-radius: 0.5rem;
}
.comic-viewer {
border: 1px solid red;
}
// comicvine metadata
.comicvine-metadata {
background-color: #f2f1f9;
@@ -410,20 +418,34 @@ pre {
}
}
// Library
.sticky {
.header-area {
width: 100%;
padding: 25px 0 15px 0;
position: sticky;
top: 57px;
z-index: 2;
z-index:9999;
background: #fffffc;
top: 50px;
}
.library {
.table-controls {
background: #fffffc;
justify-content: space-between;
position: sticky;
top: 126px;
padding-bottom: 10px;
}
.pagination {
margin: 0;
background: #fffffc;
}
table {
border-collapse: separate;
width: 100%;
thead {
position: sticky;
top: 146px;
top: 250px;
z-index: 1;
background: #fffffc;
min-height: 130px;

View File

@@ -4,11 +4,26 @@ import { useDispatch } from "react-redux";
import { saveSettings, deleteSettings } from "../../actions/settings.actions";
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import { isUndefined, isEmpty } from "lodash";
import { isUndefined, isEmpty, isNil } from "lodash";
export const AirDCPPSettingsForm = (): ReactElement => {
const dispatch = useDispatch();
const airDCPPSettings = useContext(AirDCPPSocketContext);
const hostValidator = (hostname: string): string | null => {
const hostnameRegex = /[\W]+/gm;
try {
if (!isUndefined(hostname)) {
const matches = hostname.match(hostnameRegex);
return isNil(matches) && matches.length !== 0
? hostname
: "Invalid hostname; it should not contain special characters";
}
} catch {
return null;
}
};
const onSubmit = useCallback(async (values) => {
try {
airDCPPSettings.setSettings(values);
@@ -52,12 +67,31 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</Field>
</span>
</p>
<p className="control is-expanded">
<div className="control is-expanded">
<Field name="hostname" validate={hostValidator}>
{({ input, meta }) => (
<div>
<input
{...input}
type="text"
placeholder="AirDC++ hostname"
className="input"
/>
{meta.error && meta.touched && (
<span className="is-size-7 has-text-danger">
{meta.error}
</span>
)}
</div>
)}
</Field>
</div>
<p className="control">
<Field
name="hostname"
name="port"
component="input"
className="input"
placeholder="AirDC++ host IP / hostname"
placeholder="AirDC++ port"
/>
</p>
</div>

View File

@@ -1,16 +1,12 @@
import React, { ReactElement, useContext, useEffect } from "react";
import Dashboard from "./Dashboard/Dashboard";
import Import from "./Import";
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
import LibraryContainer from "./Library/LibraryContainer";
import TabulatedContentContainer from "./Library/TabulatedContentContainer";
import LibraryGrid from "./Library/LibraryGrid";
import Search from "./Search";
import Settings from "./Settings";
import VolumeDetail from "./VolumeDetail/VolumeDetail";
import PullList from "./PullList/PullList";
import WantedComics from "./WantedComics/WantedComics";
import Volumes from "./Volumes/Volumes";
import Downloads from "./Downloads/Downloads";
import { Routes, Route } from "react-router-dom";
@@ -20,18 +16,27 @@ import {
AirDCPPSocketContextProvider,
AirDCPPSocketContext,
} from "../context/AirDCPPSocket";
import { isEmpty, isUndefined } from "lodash";
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types";
import { useDispatch } from "react-redux";
import axios from "axios";
import { LIBRARY_SERVICE_BASE_URI } from "../constants/endpoints";
import { useParams } from "react-router";
import { SocketIOProvider } from "../context/SocketIOContext";
import socketIOConnectionInstance from "../shared/socket.io/instance";
import { isEmpty, isNil, isUndefined } from "lodash";
import {
AIRDCPP_DOWNLOAD_PROGRESS_TICK,
LS_SINGLE_IMPORT,
} from "../constants/action-types";
import { useDispatch, useSelector } from "react-redux";
/**
* Method that initializes an AirDC++ socket connection
* 1. Initializes event listeners for download init, tick and complete events
* 2. Handles errors in case the connection to AirDC++ is not established or terminated
* @returns void
*/
const AirDCPPSocketComponent = (): ReactElement => {
const airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch();
useEffect(() => {
const foo = async () => {
const initializeAirDCPPEventListeners = async () => {
if (
!isUndefined(airDCPPConfiguration.airDCPPState) &&
!isEmpty(airDCPPConfiguration.airDCPPState.settings) &&
@@ -42,9 +47,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
"queue_bundle_added",
async (data) => {
console.log("JEMEN:", data);
}
},
);
// download tick listener
await airDCPPConfiguration.airDCPPState.socket.addListener(
@@ -62,9 +65,18 @@ const AirDCPPSocketComponent = (): ReactElement => {
`queue`,
"queue_bundle_status",
async (bundleData) => {
let count = 0;
if (bundleData.status.completed && bundleData.status.downloaded) {
// dispatch the action for raw import, with the metadata
console.log("IM THE MAN UP IN THIS")
if (count < 1) {
console.log(`[AirDCPP]: Download complete.`);
dispatch({
type: LS_SINGLE_IMPORT,
meta: { remote: true },
data: bundleData,
});
count += 1;
}
}
},
);
@@ -79,38 +91,70 @@ const AirDCPPSocketComponent = (): ReactElement => {
);
}
};
foo();
initializeAirDCPPEventListeners();
}, [airDCPPConfiguration]);
return <></>;
};
export const App = (): ReactElement => {
const dispatch = useDispatch();
useEffect(() => {
// Check if there is a sessionId in localStorage
const sessionId = localStorage.getItem("sessionId");
if (!isNil(sessionId)) {
// Resume the session
dispatch({
type: "RESUME_SESSION",
meta: { remote: true },
session: { sessionId },
});
} else {
// Inititalize the session and persist the sessionId to localStorage
socketIOConnectionInstance.on("sessionInitialized", (sessionId) => {
localStorage.setItem("sessionId", sessionId);
});
}
}, []);
return (
<AirDCPPSocketContextProvider>
<div>
<AirDCPPSocketComponent />
<Navbar />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} />
<Route path="/library" element={<LibraryContainer />} />
<Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} />
<Route
path={"/comic/details/:comicObjectId"}
element={<ComicDetailContainer />}
/>
<Route
path={"/volume/details/:comicObjectId"}
element={<VolumeDetail />}
/>
<Route path="/settings" element={<Settings />} />
<Route path="/pull-list/all" element={<PullList />} />
<Route path="/wanted/all" element={<WantedComics />} />
<Route path="/volumes/all" element={<Volumes />} />
</Routes>
</div>
</AirDCPPSocketContextProvider>
<SocketIOProvider socket={socketIOConnectionInstance}>
<AirDCPPSocketContextProvider>
<div>
<AirDCPPSocketComponent />
<Navbar />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} />
<Route
path="/library"
element={<TabulatedContentContainer category="library" />}
/>
<Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} />
<Route
path={"/comic/details/:comicObjectId"}
element={<ComicDetailContainer />}
/>
<Route
path={"/volume/details/:comicObjectId"}
element={<VolumeDetail />}
/>
<Route path="/settings" element={<Settings />} />
<Route
path="/pull-list/all"
element={<TabulatedContentContainer category="pullList" />}
/>
<Route
path="/wanted/all"
element={<TabulatedContentContainer category="wanted" />}
/>
<Route
path="/volumes/all"
element={<TabulatedContentContainer category="volumes" />}
/>
</Routes>
</div>
</AirDCPPSocketContextProvider>
</SocketIOProvider>
);
};

View File

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

View File

@@ -18,7 +18,6 @@ export const Menu = (props): ReactElement => {
} else if (!isEmpty(data.sourcedMetadata)) {
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
}
dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery));
setSlidingPanelContentId("CVMatches");
setVisible(true);

View File

@@ -5,6 +5,7 @@ import Card from "../Carda";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { RawFileDetails } from "./RawFileDetails";
import { ComicVineSearchForm } from "../ComicVineSearchForm";
import TabControls from "./TabControls";
import { EditMetadataPanel } from "./EditMetadataPanel";
@@ -40,6 +41,15 @@ type ComicDetailProps = {};
*/
export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
},
userSettings,
} = data;
const [page, setPage] = useState(1);
const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
@@ -56,7 +66,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
);
const extractedComicBook = useSelector(
(state: RootState) => state.fileOps.extractedComicBookArchive,
(state: RootState) => state.fileOps.extractedComicBookArchive.reading,
);
const { comicObjectId } = useParams<{ comicObjectId: string }>();
@@ -64,12 +74,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const openModal = useCallback((filePath) => {
setIsOpen(true);
dispatch(extractComicArchive(filePath));
dispatch(
extractComicArchive(filePath, {
type: "full",
purpose: "reading",
imageResizeOptions: {
baseWidth: 1024,
},
}),
);
}, []);
const afterOpenModal = useCallback(() => {
const afterOpenModal = useCallback((things) => {
// references are now sync'd and can be accessed.
// subtitle.style.color = "#f00";
console.log("kolaveri", things);
}, []);
const closeModal = useCallback(() => {
@@ -79,9 +98,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
// sliding panel init
const contentForSlidingPanel = {
CVMatches: {
content: () => {
if (!comicVineAPICallProgress) {
return (
content: (props) => (
<>
<div className="card search-criteria-card">
<div className="card-content">
<ComicVineSearchForm data={rawFileDetails} />
</div>
</div>
<p className="is-size-5 mt-3 mb-2 ml-3">Searching for:</p>
{inferredMetadata.issue ? (
<div className="ml-3">
<span className="tag mr-3">{inferredMetadata.issue.name} </span>
<span className="tag"> # {inferredMetadata.issue.number} </span>
</div>
) : null}
{!comicVineAPICallProgress ? (
<ComicVineMatchPanel
props={{
comicVineSearchQueryObject,
@@ -90,9 +121,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
comicObjectId,
}}
/>
);
} else {
return (
) : (
<div className="progress-indicator-container">
<div className="indicator">
<Loader
@@ -105,23 +134,15 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
/>
</div>
</div>
);
}
},
)}
</>
),
},
editComicBookMetadata: {
content: () => <EditMetadataPanel />,
},
};
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
},
userSettings,
} = data;
// check for the availability of CV metadata
const isComicBookMetadataAvailable =
@@ -238,7 +259,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
</div>
</div>
{/* raw file details */}
<div className="column is-three-fifths">
<div className="column">
{!isUndefined(rawFileDetails) &&
!isEmpty(rawFileDetails.cover) && (
<>
@@ -269,6 +290,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<ComicViewer
pages={extractedComicBook}
direction="ltr"
className={{closeButton: "border: 1px solid red;"}}
/>
)}
</Modal>
@@ -295,4 +317,4 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
);
};
export default ComicDetail;
export default ComicDetail;

View File

@@ -7,7 +7,7 @@ import Card from "../Carda";
export const ComicVineDetails = (props): ReactElement => {
const { data, updatedAt } = props;
return (
<div className="column is-two-thirds">
<div className="column is-half">
<div className="comic-detail comicvine-metadata">
<dl>
<dt>ComicVine Metadata</dt>

View File

@@ -10,38 +10,8 @@ export const ComicVineMatchPanel = (comicVineData): ReactElement => {
comicVineAPICallProgress,
comicVineSearchResults,
} = comicVineData.props;
console.log(comicVineData);
return (
<>
{!isEmpty(comicVineSearchQueryObject) && (
<div className="card search-criteria-card">
<div className="card-content">
<ComicVineSearchForm />
<p className="is-size-6">Searching against:</p>
<div className="field is-grouped is-grouped-multiline">
<div className="control">
<div className="tags has-addons">
<span className="tag">Title</span>
<span className="tag is-info">
{comicVineSearchQueryObject.issue.inferredIssueDetails.name}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Number</span>
<span className="tag is-info">
{
comicVineSearchQueryObject.issue.inferredIssueDetails
.number
}
</span>
</div>
</div>
</div>
</div>
</div>
)}
<div className="search-results-container">
{!isEmpty(comicVineSearchResults) && (
<MatchResult

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,9 @@ export const ComicInfoXML = (data): ReactElement => {
<dd>
<span className="is-size-7">{json.notes[0]}</span>
</dd>
<dd className="mt-1 mb-1">{json.summary[0]}</dd>
<dd className="mt-1 mb-1">
{!isUndefined(json.summary) && json.summary[0]}
</dd>
</dl>
</div>
);

View File

@@ -1,22 +1,31 @@
import React from "react";
import React, { useCallback } from "react";
import { Form, Field } from "react-final-form";
import Collapsible from "react-collapsible";
import { fetchComicVineMatches } from "../actions/fileops.actions";
import { useDispatch } from "react-redux";
/**
* Component for accepting ComicVine search parameters
* Component for performing search against ComicVine
*
* @component
* @example
* const age = 21
* const name = 'Jitendra Nirnejak'
* return (
* <User age={age} name={name} />
* <ComicVineSearchForm data={rawFileDetails} />
* )
*/
export const ComicVineSearchForm = () => {
const onSubmit = () => {
return true;
};
export const ComicVineSearchForm = (data) => {
const dispatch = useDispatch();
const onSubmit = useCallback((value) => {
const userInititatedQuery = {
inferredIssueDetails: {
name: value.issueName,
number: value.issueNumber,
subtitle: "",
year: value.issueYear,
},
};
dispatch(fetchComicVineMatches(data, userInititatedQuery));
}, []);
const validate = () => {
return true;
};
@@ -28,7 +37,7 @@ export const ComicVineSearchForm = () => {
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<span className="field is-normal">
<label className="label">Issue Details</label>
<label className="label mb-2 is-size-5">Search Manually</label>
</span>
<div className="field is-horizontal">
<div className="field-body">
@@ -48,11 +57,15 @@ export const ComicVineSearchForm = () => {
)}
</Field>
</div>
</div>
</div>
<div className="field is-horizontal">
<div className="field-body">
<div className="field">
<Field name="issueNumber">
{(props) => (
<p className="control is-expanded has-icons-left">
<p className="control has-icons-left">
<input
{...props.input}
className="input is-normal"
@@ -65,18 +78,36 @@ export const ComicVineSearchForm = () => {
)}
</Field>
</div>
<div className="field">
<Field name="issueYear">
{(props) => (
<p className="control has-icons-left">
<input
{...props.input}
className="input is-normal"
placeholder="Type the issue year"
/>
<span className="icon is-small is-left">
<i className="fas fa-hashtag"></i>
</span>
</p>
)}
</Field>
</div>
</div>
</div>
<div className="field is-horizontal">
<div className="field-body">
<div className="field">
<div className="control">
<button
type="submit"
className="button is-info is-light is-outlined is-small"
className="button is-success is-light is-outlined is-small"
>
<span className="icon">
<i className="fas fa-hand-sparkles"></i>
<i className="fas fa-search"></i>
</span>
<span>Search</span>
</button>
@@ -89,16 +120,7 @@ export const ComicVineSearchForm = () => {
/>
);
return (
<Collapsible
trigger={"Match Manually"}
triggerTagName="a"
triggerClassName={"is-size-6"}
triggerOpenedClassName={"is-size-6"}
>
<MyForm />
</Collapsible>
);
return <MyForm />;
};
export default ComicVineSearchForm;

View File

@@ -11,7 +11,8 @@ import {
getComicBooks,
} from "../../actions/fileops.actions";
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
import { isEmpty } from "lodash";
import { isEmpty, isNil } from "lodash";
import Header from "../Header";
export const Dashboard = (): ReactElement => {
const dispatch = useDispatch();
@@ -60,10 +61,40 @@ export const Dashboard = (): ReactElement => {
<section className="section">
<h1 className="title">Dashboard</h1>
{!isEmpty(recentComics) && !isEmpty(recentComics.docs) ? (
{!isEmpty(recentComics) ? (
<>
{/* Pull List */}
<PullList issues={recentComics} />
<>
<div className="content mt-6">
<Header
headerContent="Import Activity"
subHeaderContent="Results aggregated from the last import"
iconClassNames="fa-solid fa-file-invoice mr-2"
/>
</div>
<table className="table">
<thead>
<tr>
<th>
<abbr title="Position">Pos</abbr>
</th>
<th>Team</th>
<th>
<abbr title="Played">Pld</abbr>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td>38</td>
</tr>
</tbody>
</table>
</>
{/* Stats */}
{!isEmpty(libraryStatistics) && (
@@ -74,9 +105,8 @@ export const Dashboard = (): ReactElement => {
<WantedComicsList comics={wantedComics} />
)}
{/* Recent imports */}
{!isEmpty(recentComics) && (
<RecentlyImported comicBookCovers={recentComics} />
)}
<RecentlyImported comicBookCovers={recentComics} />
{/* Volumes */}
{!isEmpty(volumeGroups) && (
<VolumeGroups volumeGroups={volumeGroups} />

View File

@@ -1,6 +1,7 @@
import { isNil, map } from "lodash";
import React, { createRef, ReactElement, useCallback, useEffect } from "react";
import Card from "../Carda";
import Header from "../Header";
import Masonry from "react-masonry-css";
import { useDispatch, useSelector } from "react-redux";
import { getWeeklyPullList } from "../../actions/comicinfo.actions";
@@ -20,7 +21,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => {
dispatch(
getWeeklyPullList({
startDate: "2022-8-9",
startDate: "2023-8-9",
pageSize: "15",
currentPage: "1",
}),
@@ -88,12 +89,9 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
return (
<>
<div className="content">
<h4 className="title is-4">
<i className="fa-solid fa-splotch"></i> Discover
</h4>
<p className="subtitle is-7">
Pull List aggregated for the week from League Of Comic Geeks
</p>
<Header headerContent="Discover"
subHeaderContent="Pull List aggregated for the week from League Of Comic Geeks"
iconClassNames="fa-solid fa-splotch mr-2"/>
<div className="field is-grouped">
{/* select week */}
<div className="control">
@@ -127,7 +125,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
<Slider {...settings} ref={(c) => (sliderRef = c)}>
{!isNil(pullList) &&
pullList &&
map(pullList, (issue, idx) => {
map(pullList, ({ issue }, idx) => {
return (
<Card
key={idx}

View File

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

View File

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

View File

@@ -1,9 +1,14 @@
import React, { ReactElement, useCallback, useContext, useEffect, useState } from "react";
import React, {
ReactElement,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { getTransfers } from "../../actions/airdcpp.actions";
import { useDispatch, useSelector } from "react-redux";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import { isEmpty, isNil, isUndefined } from "lodash";
import { searchIssue } from "../../actions/fileops.actions";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import MetadataPanel from "../shared/MetadataPanel";
@@ -21,7 +26,9 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
const airDCPPTransfers = useSelector(
(state: RootState) => state.airdcpp.transfers,
);
const issueBundles = useSelector((state: RootState) => state.airdcpp.issue_bundles);
const issueBundles = useSelector(
(state: RootState) => state.airdcpp.issue_bundles,
);
const [bundles, setBundles] = useState([]);
// Make the call to get all transfers from AirDC++
useEffect(() => {
@@ -38,62 +45,86 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
useEffect(() => {
if (!isUndefined(issueBundles)) {
const foo = issueBundles.data.map((bundle) => {
const { rawFileDetails, inferredMetadata, acquisition: { directconnect: { downloads } }, sourcedMetadata: { locg, comicvine } } = bundle;
const {
rawFileDetails,
inferredMetadata,
acquisition: {
directconnect: { downloads },
},
sourcedMetadata: { locg, comicvine },
} = bundle;
const { issueName, url } = determineCoverFile({
rawFileDetails, comicvine, locg,
rawFileDetails,
comicvine,
locg,
});
return { ...bundle, issueName, url }
})
return { ...bundle, issueName, url };
});
setBundles(foo);
}
}, [issueBundles]);
}, [issueBundles])
return !isNil(bundles) ?
return !isNil(bundles) ? (
<div className="container">
<section className="section">
<h1 className="title">Downloads</h1>
<div className="columns">
<div className="column is-half">
{bundles.map(bundle => {
{bundles.map((bundle, idx) => {
console.log(bundle);
return <>
<MetadataPanel
data={bundle}
imageStyle={{ maxWidth: 80 }}
titleStyle={{ fontSize: "0.8rem" }}
tagsStyle={{ fontSize: "0.7rem" }}
containerStyle={{
maxWidth: 400,
padding: 0,
margin: "0 0 8px 0",
}} />
return (
<div key={idx}>
<MetadataPanel
data={bundle}
imageStyle={{ maxWidth: 80 }}
titleStyle={{ fontSize: "0.8rem" }}
tagsStyle={{ fontSize: "0.7rem" }}
containerStyle={{
maxWidth: 400,
padding: 0,
margin: "0 0 8px 0",
}}
/>
<table className="table is-size-7">
<tr>
<th>Name</th>
<th>Size</th>
<th>Type</th>
<th>Bundle ID</th>
</tr>
{bundle.acquisition.directconnect.downloads.map((bundle) => {
return(<tr>
<td>{bundle.name}</td>
<td>{bundle.size}</td>
<td>{bundle.type.str}</td>
<td><span className="tag is-warning">{bundle.bundleId}</span></td>
</tr>)
})}
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Type</th>
<th>Bundle ID</th>
</tr>
</thead>
<tbody>
{bundle.acquisition.directconnect.downloads.map(
(bundle, idx) => {
return (
<tr key={idx}>
<td>{bundle.name}</td>
<td>{bundle.size}</td>
<td>{bundle.type.str}</td>
<td>
<span className="tag is-warning">
{bundle.bundleId}
</span>
</td>
</tr>
);
},
)}
</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>
</section>
</div> : <div>asd</div>;
</div>
) : (
<div>There are no downloads.</div>
);
};
export default Downloads;

View File

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

View File

@@ -0,0 +1,20 @@
import React, { ReactElement } from "react";
type IHeaderProps = {
headerContent: string;
subHeaderContent: string;
iconClassNames: string;
};
export const Header = (props: IHeaderProps): ReactElement => {
return (
<>
<h4 className="title is-4">
<i className={props.iconClassNames}></i> {props.headerContent}
</h4>
<p className="subtitle is-7">{props.subHeaderContent}</p>
</>
);
};
export default Header;

View File

@@ -1,11 +1,14 @@
import React, { ReactElement, useCallback, useContext, useState } from "react";
import React, { ReactElement, useCallback, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
fetchComicBookMetadata,
toggleImportQueueStatus,
getImportJobResultStatistics,
setQueueControl,
} from "../actions/fileops.actions";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import { format } from "date-fns";
import Loader from "react-loader-spinner";
import { isEmpty, isNil, isUndefined } from "lodash";
interface IProps {
matches?: unknown;
@@ -29,102 +32,203 @@ interface IProps {
export const Import = (props: IProps): ReactElement => {
const dispatch = useDispatch();
const libraryQueueResults = useSelector(
(state: RootState) => state.fileOps.librarySearchResultCount,
const successfulImportJobCount = useSelector(
(state: RootState) => state.fileOps.successfulJobCount,
);
const failedImportJobCount = useSelector(
(state: RootState) => state.fileOps.failedJobCount,
);
const lastQueueJob = useSelector(
(state: RootState) => state.fileOps.lastQueueJob,
);
const libraryQueueImportStatus = useSelector(
(state: RootState) => state.fileOps.IMSCallInProgress,
(state: RootState) => state.fileOps.LSQueueImportStatus,
);
const [isImportQueuePaused, setImportQueueStatus] = useState(false);
const allImportJobResults = useSelector(
(state: RootState) => state.fileOps.importJobStatistics,
);
const initiateImport = useCallback(() => {
if (typeof props.path !== "undefined") {
dispatch(fetchComicBookMetadata(props.path));
}
}, [dispatch]);
const toggleImport = useCallback(() => {
setImportQueueStatus(!isImportQueuePaused);
if (isImportQueuePaused === false) {
dispatch(toggleImportQueueStatus({ action: "resume" }));
} else if (isImportQueuePaused === true) {
dispatch(toggleImportQueueStatus({ action: "pause" }));
const toggleQueue = useCallback(
(queueAction: string, queueStatus: string) => {
dispatch(setQueueControl(queueAction, queueStatus));
},
[],
);
useEffect(() => {
dispatch(getImportJobResultStatistics());
}, []);
const renderQueueControls = (status: string): ReactElement | null => {
switch (status) {
case "running":
return (
<div className="control">
<button
className="button is-warning is-light"
onClick={() => toggleQueue("pause", "paused")}
>
<i className="fa-solid fa-pause mr-2"></i> Pause
</button>
</div>
);
case "paused":
return (
<div className="control">
<button
className="button is-success is-light"
onClick={() => toggleQueue("resume", "running")}
>
<i className="fa-solid fa-play mr-2"></i> Resume
</button>
</div>
);
case "drained":
return null;
default:
return null;
}
}, [isImportQueuePaused]);
const pauseIconText = (
<>
<i className="fa-solid fa-pause mr-2"></i> Pause Import
</>
);
const playIconText = (
<>
<i className="fa-solid fa-play mr-2"></i> Resume Import
</>
);
};
return (
<div className="container">
<section className="section is-small">
<h1 className="title">Import</h1>
<h1 className="title">Import Comics</h1>
<article className="message is-dark">
<div className="message-body">
<p className="mb-2">
<span className="tag is-medium is-info is-light">
Import Only
Import Comics
</span>
will add comics identified from the mapped folder into the local
db.
will add comics identified from the mapped folder into ThreeTwo's
database.
</p>
<p>
<span className="tag is-medium is-info is-light">
Import and Tag
</span>
will scan the ComicVine, shortboxed APIs and import comics from
the mapped folder with the additional metadata.
Metadata from ComicInfo.xml, if present, will also be extracted.
</p>
<p>
This process could take a while, if you have a lot of comics, or
are importing over a network connection.
</p>
</div>
</article>
<p className="buttons">
<button
className={
libraryQueueImportStatus
? "button is-loading is-medium"
: "button is-medium"
libraryQueueImportStatus === "drained" ||
libraryQueueImportStatus === undefined
? "button is-medium"
: "button is-loading is-medium"
}
onClick={initiateImport}
>
<span className="icon">
<i className="fas fa-file-import"></i>
</span>
<span>Import Only</span>
</button>
<button className="button is-medium">
<span className="icon">
<i className="fas fa-tag"></i>
</span>
<span>Import and Tag</span>
<span>Start Import</span>
</button>
</p>
<div className="columns is-multiline">
<div className="column is-one-fifth">
<div className="box control-palette">
<span className="is-size-2 has-text-weight-bold">
{JSON.stringify(libraryQueueResults, null, 2)}
</span>
</div>
<div className="is-half">
<div className="content">
<div className="control">
<button
className="button is-warning is-light"
onClick={toggleImport}
>
{!isImportQueuePaused ? pauseIconText : playIconText}
</button>
</div>
</div>
</div>
</div>
</div>
{libraryQueueImportStatus !== "drained" &&
!isUndefined(libraryQueueImportStatus) && (
<>
<table className="table">
<thead>
<tr>
<th>Completed Jobs</th>
<th>Failed Jobs</th>
<th>Queue Controls</th>
<th>Queue Status</th>
</tr>
</thead>
<tbody>
<tr>
<th>
{successfulImportJobCount > 0 && (
<div className="box has-background-success-light has-text-centered">
<span className="is-size-2 has-text-weight-bold">
{successfulImportJobCount}
</span>
</div>
)}
</th>
<td>
{failedImportJobCount > 0 && (
<div className="box has-background-danger has-text-centered">
<span className="is-size-2 has-text-weight-bold">
{failedImportJobCount}
</span>
</div>
)}
</td>
<td>{renderQueueControls(libraryQueueImportStatus)}</td>
<td>
{libraryQueueImportStatus !== undefined ? (
<span className="tag is-warning">
{libraryQueueImportStatus}
</span>
) : null}
</td>
</tr>
</tbody>
</table>
Imported{" "}
<span className="has-text-weight-bold">{lastQueueJob}</span>
</>
)}
{/* Past imports */}
<h3 className="subtitle is-4 mt-5">Past Imports</h3>
<table className="table">
<thead>
<tr>
<th>Time Started</th>
<th>Session Id</th>
<th>Imported</th>
<th>Failed</th>
</tr>
</thead>
<tbody>
{allImportJobResults.map((jobResult, id) => {
return (
<tr key={id}>
<td>
{format(
new Date(jobResult.earliestTimestamp),
"EEEE, hh:mma, do LLLL Y",
)}
</td>
<td>
<span className="tag is-warning">
{jobResult.sessionId}
</span>
</td>
<td>
<span className="tag is-success">
{jobResult.completedJobs}
</span>
</td>
<td>
<span className="tag is-danger">
{jobResult.failedJobs}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</section>
</div>
);

View File

@@ -1,22 +1,43 @@
import React, { useMemo, ReactElement, useCallback } from "react";
import React, { useMemo, ReactElement, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import T2Table from "../shared/T2Table";
import { isEmpty, isNil, isUndefined } from "lodash";
import MetadataPanel from "../shared/MetadataPanel";
import SearchBar from "./SearchBar";
import { useDispatch } from "react-redux";
import T2Table from "../shared/T2Table";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
import ellipsize from "ellipsize";
interface IComicBookLibraryProps {
data: {
searchResults: any;
};
}
export const Library = (data: IComicBookLibraryProps): ReactElement => {
const { searchResults } = data.data;
/**
* Component that tabulates the contents of the user's ThreeTwo Library.
*
* @component
* @example
* <Library />
*/
export const Library = (): ReactElement => {
const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics,
);
const searchError = useSelector((state: RootState) => state.fileOps.librarySearchError);
const dispatch = useDispatch();
useEffect(() => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: 15,
from: 0,
},
type: "all",
trigger: "libraryPage",
},
),
);
}, []);
// programatically navigate to comic detail
const navigate = useNavigate();
@@ -26,7 +47,7 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
const ComicInfoXML = (value) => {
return value.data ? (
<div className="comicvine-metadata">
<div className="comicvine-metadata mt-3">
<dl>
<span className="tags has-addons is-size-7">
<span className="tag">Series</span>
@@ -65,7 +86,8 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
const WantedStatus = ({ value }) => {
return !value ? <span className="tag is-info is-light">Wanted</span> : null;
};
const columns = [
const columns = useMemo(
() => [
{
header: "Comic Metadata",
footer: 1,
@@ -75,7 +97,7 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
id: "fileDetails",
minWidth: 400,
accessorKey: "_source",
cell: info => {
cell: (info) => {
return <MetadataPanel data={info.getValue()} />;
},
},
@@ -84,11 +106,11 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
accessorKey: "_source.sourcedMetadata.comicInfo",
align: "center",
minWidth: 250,
cell: info =>
cell: (info) =>
!isEmpty(info.getValue()) ? (
<ComicInfoXML data={info.getValue()} />
) : (
<span className="tag">No ComicInfo.xml</span>
<span className="tag mt-5">No ComicInfo.xml</span>
),
},
],
@@ -98,13 +120,12 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
columns: [
{
header: "Publisher",
accessorKey:
"_source.sourcedMetadata.comicvine.volumeInformation",
cell: info => {
accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation",
cell: (info) => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{ info.getValue().publisher.name }
{info.getValue().publisher.name}
</h6>
)
);
@@ -113,21 +134,29 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
{
header: "Something",
accessorKey: "_source.acquisition.source.wanted",
cell: info => {
!isUndefined(info.getValue()) ?
<WantedStatus value={info.getValue().toString()} /> : "Nothing";
cell: (info) => {
!isUndefined(info.getValue()) ? (
<WantedStatus value={info.getValue().toString()} />
) : (
"Nothing"
);
},
},
],
}
]
},
],
[],
);
// ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired,
// };
const dispatch = useDispatch();
const goToNextPage = useCallback((pageIndex, pageSize) => {
/**
* Pagination control that fetches the next x (pageSize) items
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
*
**/
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch(
searchIssue(
{
@@ -139,17 +168,25 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from: pageSize * pageIndex + 1,
},
type: "all",
trigger: "libraryPage",
},
),
);
}, []);
const goToPreviousPage = useCallback((pageIndex, pageSize) => {
/**
* Pagination control that fetches the previous x (pageSize) items
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
**/
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
let from = 0;
if (pageIndex === 2) {
from = (pageIndex - 1) * pageSize + 2 - 27;
from = (pageIndex - 1) * pageSize + 2 - 17;
} else {
from = (pageIndex - 1) * pageSize + 2 - 26;
from = (pageIndex - 1) * pageSize + 2 - 16;
}
dispatch(
searchIssue(
@@ -162,30 +199,54 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from,
},
type: "all",
trigger: "libraryPage",
},
),
);
}, []);
// ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired,
// };
return (
<section className="container">
<div className="section">
<h1 className="title">Library</h1>
{/* Search bar */}
<SearchBar />
{!isUndefined(searchResults) && (
<div className="header-area">
<h1 className="title">Library</h1>
</div>
{!isEmpty(searchResults) ? (
<div>
<div className="library">
<T2Table
rowData={searchResults.hits.hits}
totalPages={searchResults.hits.total.value}
totalPages={searchResults.total.value}
columns={columns}
paginationHandlers={{
nextPage: goToNextPage,
previousPage: goToPreviousPage,
}}
sourceData={searchResults?.hits}
rowClickHandler={navigateToComicDetail}
paginationHandlers={{
nextPage,
previousPage,
}}
/>
{/* pagination controls */}
</div>
</div>
) : (
<div className="columns">
<div className="column is-two-thirds">
<article className="message is-link">
<div className="message-body">
No comics were found in the library, Elasticsearch reports no
indices. Try importing a few comics into the library and come
back.
</div>
</article>
<pre>
{!isUndefined(searchError.data) &&
JSON.stringify(
searchError.data.meta.body.error.root_cause,
null,
4,
)}
</pre>
</div>
</div>
)}

View File

@@ -1,63 +0,0 @@
import { isEmpty, isUndefined } from "lodash";
import React, { ReactElement, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
import { Library } from "./Library";
const LibraryContainer = (): ReactElement => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: 25,
from: 0,
},
type: "all",
trigger: "libraryPage"
},
),
);
}, []);
const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics,
);
const searchError = useSelector(
(state: RootState) => state.fileOps.librarySearchError,
);
return !isEmpty(searchResults) ? (
<Library data={{ searchResults }} />
) : (
<div className="container">
<section className="section is-small">
<div className="columns">
<div className="column is-two-thirds">
<article className="message is-link">
<div className="message-body">
No comics were found in the library, Elasticsearch reports no
indices. Try importing a few comics into the library and come
back.
</div>
</article>
<pre>
{!isUndefined(searchError.data) &&
JSON.stringify(
searchError.data.meta.body.error.root_cause,
null,
4,
)}
</pre>
</div>
</div>
</section>
</div>
);
};
export default LibraryContainer;

View File

@@ -73,7 +73,7 @@ export const LibraryGrid = (libraryGridProps: ILibraryGridProps) => {
<div className="content is-flex is-flex-direction-row">
{!isEmpty(sourcedMetadata.comicvine) && (
<span className="icon cv-icon is-small">
<img src="/dist/img/cvlogo.svg" />
<img src="/src/client/assets/img/cvlogo.svg" />
</span>
)}
{isNil(rawFileDetails) && (

View File

@@ -21,19 +21,20 @@ export const SearchBar = (): ReactElement => {
from: 0,
},
type: "volumeName",
trigger: "libraryPage",
},
),
);
}, []);
return (
<div className="box sticky">
<div className="box">
<Form
onSubmit={handleSubmit}
initialValues={{}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<div className="field is-grouped">
<div className="control is-expanded search">
<div className="control search is-expanded">
<Field name="search">
{({ input, meta }) => {
return (
@@ -55,26 +56,7 @@ export const SearchBar = (): ReactElement => {
</form>
)}
/>
{/* <div className="column one-fifth">
<div className="field has-addons">
<p className="control">
<button className="button">
<span className="icon is-small">
<i className="fa-solid fa-list"></i>
</span>
</button>
</p>
<p className="control">
<button className="button">
<Link to="/library-grid">
<span className="icon is-small">
<i className="fa-solid fa-image"></i>
</span>
</Link>
</button>
</p>
</div>
</div> */}
</div>
);
};

View File

@@ -0,0 +1,66 @@
import React, { ReactElement } from "react";
import PullList from "../PullList/PullList";
import { Volumes } from "../Volumes/Volumes";
import WantedComics from "../WantedComics/WantedComics";
import { Library } from "./Library";
interface ITabulatedContentContainerProps {
category: string;
}
/**
* Component to draw the contents of a category in a table.
*
* @component
* @example
* return (
* <TabulatedContentContainer category={"library"} />
* )
*/
const TabulatedContentContainer = (
props: ITabulatedContentContainerProps,
): ReactElement => {
const { category } = props;
const renderTabulatedContent = () => {
switch (category) {
case "library":
return <Library />;
case "pullList":
return <PullList />;
case "wanted":
return <WantedComics />;
case "volumes":
return <Volumes />;
default:
return <></>;
}
};
return renderTabulatedContent();
// : (
// <div className="container">
// <section className="section is-small">
// <div className="columns">
// <div className="column is-two-thirds">
// <article className="message is-link">
// <div className="message-body">
// No comics were found in the library, and Elasticsearch doesn't have any
// indices. Try resetting the library from <code>Settings > Flush DB & Temporary Folders</code> and then import your library again.
// </div>
// </article>
// <pre>
// {!isUndefined(searchError.data) &&
// JSON.stringify(
// searchError.data.meta.body.error.root_cause,
// null,
// 4,
// )}
// </pre>
// </div>
// </div>
// </section>
// </div>
// );
};
export default TabulatedContentContainer;

View File

@@ -1,21 +1,50 @@
import React from "react";
import React, { useContext } from "react";
import { SearchBar } from "./GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { isUndefined } from "lodash";
import { format, fromUnixTime } from "date-fns";
const Navbar: React.FunctionComponent = (props) => {
const downloadProgressTick = useSelector(
(state: RootState) => state.airdcpp.downloadProgressData,
);
const airDCPPSocketConnectionStatus = useSelector(
(state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
);
const airDCPPSessionInfo = useSelector(
(state: RootState) => state.airdcpp.airDCPPSessionInfo,
);
const socketDisconnectionReason = useSelector(
(state: RootState) => state.airdcpp.socketDisconnectionReason,
);
// Import-related selector hooks
const successfulImportJobCount = useSelector(
(state: RootState) => state.fileOps.successfulJobCount,
);
const failedImportJobCount = useSelector(
(state: RootState) => state.fileOps.failedJobCount,
);
const lastQueueJob = useSelector(
(state: RootState) => state.fileOps.lastQueueJob,
);
const libraryQueueImportStatus = useSelector(
(state: RootState) => state.fileOps.LSQueueImportStatus,
);
const allImportJobResults = useSelector(
(state: RootState) => state.fileOps.importJobStatistics,
);
return (
<nav className="navbar is-fixed-top">
<div className="navbar-brand">
<Link to="/" className="navbar-item">
<img
src="/img/threetwo.svg"
src="/src/client/assets/img/threetwo.svg"
alt="ThreeTwo! A comic book curator"
width="112"
height="28"
@@ -72,18 +101,100 @@ const Navbar: React.FunctionComponent = (props) => {
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i>
{downloadProgressTick && (
<div className="pulsating-circle"></div>
)}
{downloadProgressTick && <div className="pulsating-circle"></div>}
</a>
{!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown download-progress-meter">
<div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} />
</a>
</div>
) : null}
</div>
{!isUndefined(libraryQueueImportStatus) &&
location.hash !== "#/import" ? (
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-file-import has-text-warning-dark"></i>
</a>
<div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item">
<ul>
{successfulImportJobCount > 0 ? (
<li className="mb-2">
<span className="tag is-success mr-2">
{successfulImportJobCount}
</span>
imported.
</li>
) : null}
{failedImportJobCount > 0 ? (
<li>
<span className="tag is-danger mr-2">
{failedImportJobCount}
</span>
failed to import.
</li>
) : null}
</ul>
</a>
</div>
</div>
) : null}
{/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable">
{airDCPPSocketConnectionStatus ? (
<>
<a className="navbar-link is-arrowless has-text-success">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right airdcpp-status is-boxed">
{/* AirDC++ Session Information */}
<p>
Last login was{" "}
<span className="tag">
{format(
fromUnixTime(airDCPPSessionInfo.user.last_login),
"dd MMMM, yyyy",
)}
</span>
</p>
<hr className="navbar-divider" />
<p>
<span className="tag has-text-success">
{airDCPPSessionInfo.user.username}
</span>
connected to{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.system_info.client_version}
</span>{" "}
with session ID{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.session_id}
</span>
</p>
{/* <pre>{JSON.stringify(airDCPPSessionInfo, null, 2)}</pre> */}
</div>
</>
) : (
<>
<a className="navbar-link is-arrowless has-text-danger">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right is-boxed">
<pre>
{JSON.stringify(socketDisconnectionReason, null, 2)}
</pre>
</div>
</>
)}
</div>
<div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div>
<div id="blogDropdown" className="navbar-dropdown">

View File

@@ -4,18 +4,19 @@ import { getWeeklyPullList } from "../../actions/comicinfo.actions";
import { useDispatch, useSelector } from "react-redux";
import Card from "../Carda";
import ellipsize from "ellipsize";
import { isNil } from "lodash";
export const PullList = (): ReactElement => {
const pullListComics = useSelector(
(state: RootState) => state.comicInfo.pullList,
);
console.log(pullListComics);
const dispatch = useDispatch();
useEffect(() => {
dispatch(
getWeeklyPullList({
startDate: "2022-6-1",
pageSize: "100",
startDate: "2023-7-28",
pageSize: "15",
currentPage: "1",
}),
);
@@ -25,24 +26,25 @@ export const PullList = (): ReactElement => {
const columnData = useMemo(
() => [
{
Header: "Comic Information",
header: "Comic Information",
columns: [
{
Header: "Details",
header: "Details",
id: "comicDetails",
minWidth: 450,
accessor: (row) => {
console.log(row);
accessorKey: "issue",
cell: (row) => {
const item = row.getValue();
return (
<div className="columns">
<div className="column">
<div className="column is-three-quarters">
<div className="comic-detail issue-metadata">
<dl>
<dd>
<div className="columns mt-2">
<div className="column is-3">
<Card
imageUrl={row.cover}
imageUrl={item.cover}
orientation={"vertical"}
hasDetails={false}
// cardContainerStyle={{ maxWidth: 200 }}
@@ -52,18 +54,20 @@ export const PullList = (): ReactElement => {
<dl>
<dt>
<h6 className="name has-text-weight-medium mb-1">
{row.name}
{item.name}
</h6>
</dt>
<dd className="is-size-7">
published by{" "}
<span className="has-text-weight-semibold">
{row.publisher}
{item.publisher}
</span>
</dd>
<dd className="is-size-7">
<span>{ellipsize(row.description, 190)}</span>
<span>
{ellipsize(item.description, 190)}
</span>
</dd>
<dd className="is-size-7 mt-2">
@@ -71,10 +75,10 @@ export const PullList = (): ReactElement => {
<div className="control">
<span className="tags">
<span className="tag is-success is-light has-text-weight-semibold">
{row.price}
{item.price}
</span>
<span className="tag is-success is-light">
{row.pulls}
{item.pulls}
</span>
</span>
</div>
@@ -99,16 +103,24 @@ export const PullList = (): ReactElement => {
return (
<section className="container">
<div className="section">
<h1 className="title">Weekly Pull List</h1>
<T2Table
rowData={pullListComics}
columns={columnData}
totalPages={pullListComics.length}
paginationHandlers={{
nextPage: nextPageHandler,
previousPage: previousPageHandler,
}}
/>
<div className="header-area">
<h1 className="title">Weekly Pull List</h1>
</div>
{!isNil(pullListComics) && (
<div>
<div className="library">
<T2Table
sourceData={pullListComics}
columns={columnData}
totalPages={pullListComics.length}
paginationHandlers={{
nextPage: nextPageHandler,
previousPage: previousPageHandler,
}}
/>
</div>
</div>
)}
</div>
</section>
);

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback, ReactElement } from "react";
import React, { useCallback, ReactElement } from "react";
import { isNil, isEmpty } from "lodash";
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
import { importToDB } from "../actions/fileops.actions";

View File

@@ -0,0 +1,25 @@
import React, { ReactElement } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { getServiceStatus } from "../../actions/fileops.actions";
export const ServiceStatuses = (): ReactElement => {
const serviceStatus = useSelector(
(state: RootState) => state.fileOps.libraryServiceStatus,
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getServiceStatus());
}, []);
return (
<div className="is-clearfix">
<div className="mt-4">
<h3 className="title">Core Services</h3>
<h6 className="subtitle has-text-grey-light">
Statuses for core services
</h6>
</div>
<pre>{JSON.stringify(serviceStatus, null, 2)}</pre>
</div>
);
};

View File

@@ -2,6 +2,7 @@ import React, { useState, ReactElement } from "react";
import { AirDCPPSettingsForm } from "./AirDCPPSettings/AirDCPPSettingsForm";
import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
import { ServiceStatuses } from "./ServiceStatuses/ServiceStatuses";
import settingsObject from "../constants/settings/settingsMenu.json";
import { isUndefined, map } from "lodash";
@@ -22,6 +23,10 @@ export const Settings = (props: ISettingsProps): ReactElement => {
</div>
),
},
{
id: "core-service",
content: <ServiceStatuses />,
},
{
id: "flushdb",
content: (

View File

@@ -2,16 +2,13 @@ import React, { ReactElement, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
import Card from "../Carda";
import SearchBar from "../Library/SearchBar";
import T2Table from "../shared/T2Table";
import ellipsize from "ellipsize";
import { isUndefined } from "lodash";
import { convert } from "html-to-text";
import { isUndefined } from "lodash";
export const Volumes = (props): ReactElement => {
const volumes = useSelector(
(state: RootState) => state.fileOps.librarySearchResults,
);
const volumes = useSelector((state: RootState) => state.fileOps.volumes);
const dispatch = useDispatch();
useEffect(() => {
dispatch(
@@ -25,19 +22,20 @@ export const Volumes = (props): ReactElement => {
from: 0,
},
type: "volumes",
trigger: "volumesPage",
},
),
);
}, []);
console.log(volumes);
const columnData = useMemo(
() => [
{
Header: "Volume Details",
header: "Volume Details",
id: "volumeDetails",
minWidth: 450,
accessor: (row) => {
accessorKey: "_source",
cell: (row) => {
const foo = row.getValue();
return (
<div className="columns">
<div className="column">
@@ -45,11 +43,11 @@ export const Volumes = (props): ReactElement => {
<dl>
<dd>
<div className="columns mt-2">
<div className="column is-3">
<div className="">
<Card
imageUrl={
row._source.sourcedMetadata.comicvine
.volumeInformation.image.thumb_url
foo.sourcedMetadata.comicvine.volumeInformation
.image.thumb_url
}
orientation={"vertical"}
hasDetails={false}
@@ -61,7 +59,7 @@ export const Volumes = (props): ReactElement => {
<dt>
<h6 className="name has-text-weight-medium mb-1">
{
row._source.sourcedMetadata.comicvine
foo.sourcedMetadata.comicvine
.volumeInformation.name
}
</h6>
@@ -70,7 +68,7 @@ export const Volumes = (props): ReactElement => {
published by{" "}
<span className="has-text-weight-semibold">
{
row._source.sourcedMetadata.comicvine
foo.sourcedMetadata.comicvine
.volumeInformation.publisher.name
}
</span>
@@ -80,7 +78,7 @@ export const Volumes = (props): ReactElement => {
<span>
{ellipsize(
convert(
row._source.sourcedMetadata.comicvine
foo.sourcedMetadata.comicvine
.volumeInformation.description,
{
baseElements: {
@@ -102,7 +100,7 @@ export const Volumes = (props): ReactElement => {
</span>
<span className="tag is-success is-light">
{
row._source.sourcedMetadata.comicvine
foo.sourcedMetadata.comicvine
.volumeInformation.count_of_issues
}
</span>
@@ -122,36 +120,35 @@ export const Volumes = (props): ReactElement => {
},
},
{
Header: "Download Status",
header: "Download Status",
columns: [
{
Header: "Files",
accessor: "_source.acquisition.directconnect",
header: "Files",
accessorKey: "_source.acquisition.directconnect",
align: "right",
Cell: (props) => {
cell: (props) => {
const row = props.getValue();
return (
<div
style={{
display: "flex",
// flexDirection: "column",
flexDirection: "column",
justifyContent: "center",
}}
>
{props.cell.value.length > 0 ? (
<span className="tag is-warning">
{props.cell.value.length}
</span>
{row.length > 0 ? (
<span className="tag is-warning">{row.length}</span>
) : null}
</div>
);
},
},
{
Header: "Type",
header: "Type",
id: "Air",
},
{
Header: "Type",
header: "Type",
id: "dcc",
},
],
@@ -162,16 +159,24 @@ export const Volumes = (props): ReactElement => {
return (
<section className="container">
<div className="section">
{volumes.hits ? (
<div className="header-area">
<h1 className="title">Volumes</h1>
</div>
{!isUndefined(volumes.hits) && (
<div>
<div className="library">
<h1 className="title">Volumes</h1>
{/* Search bar */}
<SearchBar />
<T2Table rowData={volumes.hits.hits} columns={columnData} />
<T2Table
sourceData={volumes?.hits}
totalPages={volumes.hits.length}
paginationHandlers={{
nextPage: () => {},
previousPage: () => {},
}}
columns={columnData}
/>
</div>
</div>
) : null}
)}
</div>
</section>
);

View File

@@ -1,4 +1,4 @@
import React, { ReactElement, useEffect, useMemo } from "react";
import React, { ReactElement, useCallback, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
import SearchBar from "../Library/SearchBar";
@@ -73,8 +73,8 @@ export const WantedComics = (props): ReactElement => {
id: "downloadDetails",
accessorKey: "acquisition",
cell: data => <ol>
{data.getValue().directconnect.downloads.map(download => {
return <li className="is-size-7">{download.name}</li>;
{data.getValue().directconnect.downloads.map((download, idx) => {
return <li className="is-size-7" key={idx}>{download.name}</li>;
})}
</ol>
},
@@ -86,23 +86,81 @@ export const WantedComics = (props): ReactElement => {
},
];
/**
* Pagination control that fetches the next x (pageSize) items
* based on the y (pageIndex) offset from the Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
*
**/
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: pageSize,
from: pageSize * pageIndex + 1,
},
type: "wanted",
trigger: "wantedComicsPage",
},
),
);
}, []);
/**
* Pagination control that fetches the previous x (pageSize) items
* based on the y (pageIndex) offset from the Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
**/
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
let from = 0;
if (pageIndex === 2) {
from = (pageIndex - 1) * pageSize + 2 - 17;
} else {
from = (pageIndex - 1) * pageSize + 2 - 16;
}
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: pageSize,
from,
},
type: "wanted",
trigger: "wantedComicsPage"
},
),
);
}, []);
return (
<section className="container">
<div className="section">
<h1 className="title">Wanted Comics</h1>
{/* Search bar */}
<SearchBar />
<div className="header-area">
<h1 className="title">Wanted Comics</h1>
</div>
{!isEmpty(wantedComics) && (
<div>
<div className="library">
<T2Table
rowData={wantedComics}
sourceData={wantedComics}
totalPages={wantedComics.length}
columns={columnData}
// paginationHandlers={{
// nextPage: goToNextPage,
// previousPage: goToPreviousPage,
// }}
paginationHandlers={{
nextPage: nextPage,
previousPage: previousPage,
}}
// rowClickHandler={navigateToComicDetail}
/>
{/* pagination controls */}

View File

@@ -57,6 +57,12 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
>
{rawFileDetails.extension}
</span>
<span
className="tag is-success is-light has-text-weight-semibold"
style={props.tagsStyle}
>
{rawFileDetails.mimeType}
</span>
<span
className="tag is-success is-light"
style={props.tagsStyle}
@@ -77,7 +83,9 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
</div>
)}
</div>
</div>
</dd>
</dl>
),
@@ -211,7 +219,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
orientation={"vertical"}
hasDetails={false}
imageStyle={props.imageStyle}
// cardContainerStyle={{ maxWidth: 200 }}
// cardContainerStyle={{ maxWidth: 200 }}
/>
</div>
<div className="column">{metadataPanel.content()}</div>

View File

@@ -1,27 +1,112 @@
import React, { ReactElement, useState } from "react";
import React, { ReactElement, useMemo, useState } from "react";
import PropTypes from "prop-types";
import SearchBar from "../Library/SearchBar";
import { Link } from "react-router-dom";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
PaginationState,
} from "@tanstack/react-table";
export const T2Table = (tableOptions): ReactElement => {
const { rowData, columns, paginationHandlers, totalPages, rowClickHandler } =
const { sourceData, columns, paginationHandlers: { nextPage, previousPage }, totalPages, rowClickHandler } =
tableOptions;
const [isPageSizeDropdownCollapsed, collapsePageSizeDropdown] =
useState(false);
const togglePageSizeDropdown = () =>
collapsePageSizeDropdown(!isPageSizeDropdownCollapsed);
const [{ pageIndex, pageSize }, setPagination] =
useState<PaginationState>({
pageIndex: 1,
pageSize: 15,
});
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
);
/**
* Pagination control to move forward one page
* @returns void
*/
const goToNextPage = () => {
setPagination({
pageIndex: pageIndex + 1,
pageSize,
});
nextPage(pageIndex, pageSize);
}
/**
* Pagination control to move backward one page
* @returns void
**/
const goToPreviousPage = () => {
setPagination({
pageIndex: pageIndex - 1,
pageSize,
});
previousPage(pageIndex, pageSize);
}
const table = useReactTable({
data: rowData,
data: sourceData,
columns,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
pageCount: sourceData.length ?? -1,
state: {
pagination,
},
onPaginationChange: setPagination,
});
return (
<>
<div className="columns table-controls">
{/* Search bar */}
<div className="column is-half">
<SearchBar />
</div>
{/* pagination controls */}
<nav className="pagination columns">
<div className="mr-4 has-text-weight-semibold has-text-left">
<p className="is-size-5">Page {pageIndex} of {Math.ceil(totalPages / pageSize)}</p>
{/* <p>{totalPages} comics in all</p> */}
</div>
<div className="field has-addons">
<div className="control">
<div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div>
</div>
<div className="control">
<div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div>
</div>
<div className="field has-addons ml-5">
<p className="control">
<button className="button">
<span className="icon is-small">
<i className="fa-solid fa-list"></i>
</span>
</button>
</p>
<p className="control">
<button className="button">
<Link to="/library-grid">
<span className="icon is-small">
<i className="fa-solid fa-image"></i>
</span>
</Link>
</button>
</p>
</div>
</div>
</nav>
</div>
<table className="table is-hoverable">
<thead>
{table.getHeaderGroups().map((headerGroup, idx) => (
@@ -60,15 +145,12 @@ export const T2Table = (tableOptions): ReactElement => {
})}
</tbody>
</table>
{/* pagination control */}
</>
);
};
T2Table.propTypes = {
rowData: PropTypes.array,
sourceData: PropTypes.array,
totalPages: PropTypes.number,
columns: PropTypes.array,
paginationHandlers: PropTypes.shape({

View File

@@ -14,7 +14,6 @@ export const IMS_RAW_IMPORT_FAILED = "IMS_RAW_IMPORT_FAILED";
export const LS_IMPORT_CALL_IN_PROGRESS = "LS_IMPORT_CALL_IN_PROGRESS";
// Library import bull mq queue control
export const LS_TOGGLE_IMPORT_QUEUE = "LS_TOGGLE_IMPORT_QUEUE";
export const LS_QUEUE_DRAINED = "LS_QUEUE_DRAINED";
// ComicVine Metadata
export const IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS =
@@ -51,10 +50,12 @@ export const IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS =
"IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS";
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
export const VOLUMES_FETCHED = "VOLUMES_FETCHED";
// search results from the Search service
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";
export const SS_SEARCH_RESULTS_FETCHED_SPECIAL = "SS_SEARCH_RESULTS_FETCHED_SPECIAL";
export const SS_SEARCH_RESULTS_FETCHED_SPECIAL =
"SS_SEARCH_RESULTS_FETCHED_SPECIAL";
export const SS_SEARCH_IN_PROGRESS = "SS_SEARCH_IN_PROGRESS";
export const SS_SEARCH_FAILED = "SS_SEARCH_FAILED";
@@ -80,6 +81,8 @@ export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS =
export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED =
"IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED";
export const COMICBOOK_EXTRACTION_SUCCESS = "COMICBOOK_EXTRACTION_SUCCESS";
// Image file stats
export const IMG_ANALYSIS_CALL_IN_PROGRESS = "IMG_ANALYSIS_CALL_IN_PROGRESS";
export const IMG_ANALYSIS_DATA_FETCH_SUCCESS =
@@ -102,7 +105,7 @@ export const AIRDCPP_SEARCH_RESULTS_UPDATED = "AIRDCPP_SEARCH_RESULTS_UPDATED";
export const AIRDCPP_SEARCH_COMPLETE = "AIRDCPP_SEARCH_COMPLETE";
// AirDC++ related library query for issues with bundles associated with them
export const LIBRARY_ISSUE_BUNDLES = "LIBRARY_ISSUE_BUNDLES";
export const LIBRARY_ISSUE_BUNDLES = "LIBRARY_ISSUE_BUNDLES";
export const AIRDCPP_HUB_SEARCHES_SENT = "AIRDCPP_HUB_SEARCHES_SENT";
export const AIRDCPP_RESULT_DOWNLOAD_INITIATED =
@@ -112,6 +115,8 @@ export const AIRDCPP_FILE_DOWNLOAD_COMPLETED =
export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK";
export const AIRDCPP_SOCKET_CONNECTED = "AIRDCPP_SOCKET_CONNECTED";
export const AIRDCPP_SOCKET_DISCONNECTED = "AIRDCPP_SOCKET_DISCONNECTED";
// Transfers
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
@@ -119,10 +124,17 @@ export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
// Comics marked as "wanted"
export const WANTED_COMICS_FETCHED = "WANTED_COMICS_FETCHED";
// LIBRARY SOCKET ENDPOINT
// LIBRARY Service import queue-related action types
export const LS_IMPORT = "LS_IMPORT";
export const LS_COVER_EXTRACTED = "LS_COVER_EXTRACTED";
export const LS_COVER_EXTRACTION_FAILED = "LS_COVER_EXTRACTION_FAILED";
export const LS_COMIC_ADDED = "LS_COMIC_ADDED";
export const LS_IMPORT_QUEUE_DRAINED = "LS_IMPORT_QUEUE_DRAINED";
export const LS_SET_QUEUE_STATUS = "LS_SET_QUEUE_STATUS";
export const RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION =
"RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION";
export const LS_IMPORT_JOB_STATISTICS_FETCHED =
"LS_IMPORT_JOB_STATISTICS_FETCHED";
// Settings
export const SETTINGS_CALL_IN_PROGRESS = "SETTINGS_CALL_IN_PROGRESS";
@@ -135,3 +147,6 @@ export const SETTINGS_DB_FLUSH_SUCCESS = "SETTINGS_DB_FLUSH_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_ERROR = "METRON_DATA_FETCH_ERROR";
// service health statuses
export const LIBRARY_SERVICE_HEALTH = "LIBRARY_SERVICE_HEALTH";

View File

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

View File

@@ -57,6 +57,21 @@
},
{
"id": 4,
"category": "statuses",
"displayName": "Service Status",
"children": [
{
"id": "core-service",
"displayName": "Core Services"
},
{
"id": "metadata-service",
"displayName": "Metadata Services"
}
]
},
{
"id": 5,
"category": "system",
"displayName": "System",
"children": [
@@ -67,7 +82,7 @@
]
},
{
"id": 5,
"id": 6,
"category": "acknowledgments",
"displayName": "Acknowledgments",
"children": [

View File

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

View File

@@ -0,0 +1,11 @@
import React, { createContext } from "react";
export const SocketIOContext = createContext({});
export const SocketIOProvider = ({ children, socket }) => {
return (
<SocketIOContext.Provider value={socket}>
{children}
</SocketIOContext.Provider>
);
};

View File

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

View File

@@ -109,10 +109,14 @@ function comicinfoReducer(state = initialState, action) {
...state,
};
case CV_WEEKLY_PULLLIST_FETCHED: {
const foo = [];
action.data.map((item) => {
foo.push({issue: item})
});
return {
...state,
inProgress: false,
pullList: [...action.data],
pullList: foo,
};
}
case LIBRARY_STATISTICS_CALL_IN_PROGRESS:

View File

@@ -15,10 +15,9 @@ import {
IMS_COMIC_BOOK_GROUPS_FETCHED,
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
LS_IMPORT,
LS_COVER_EXTRACTED,
LS_QUEUE_DRAINED,
LS_COVER_EXTRACTION_FAILED,
LS_COMIC_ADDED,
IMG_ANALYSIS_CALL_IN_PROGRESS,
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
@@ -28,29 +27,47 @@ import {
LS_IMPORT_CALL_IN_PROGRESS,
SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
VOLUMES_FETCHED,
COMICBOOK_EXTRACTION_SUCCESS,
LIBRARY_SERVICE_HEALTH,
LS_IMPORT_QUEUE_DRAINED,
LS_SET_QUEUE_STATUS,
RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION,
LS_IMPORT_JOB_STATISTICS_FETCHED,
} from "../constants/action-types";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
const initialState = {
IMSCallInProgress: false,
IMGCallInProgress: false,
SSCallInProgress: false,
imageAnalysisResults: {},
comicBookExtractionInProgress: false,
LSQueueImportStatus: undefined,
comicBookMetadata: [],
comicVolumeGroups: [],
isSocketConnected: false,
isComicVineMetadataImportInProgress: false,
comicVineMetadataImportError: {},
rawImportError: {},
extractedComicBookArchive: [],
extractedComicBookArchive: {
reading: [],
analysis: [],
},
recentComics: [],
wantedComics: [],
libraryComics: [],
volumes: [],
librarySearchResultsFormatted: [],
librarySearchResultCount: 0,
lastQueueJob: "",
successfulJobCount: 0,
failedJobCount: 0,
importJobStatistics: [],
libraryQueueResults: [],
librarySearchError: {},
libraryServiceStatus: {},
};
function fileOpsReducer(state = initialState, action) {
switch (action.type) {
case IMS_COMICBOOK_METADATA_FETCHED:
@@ -79,7 +96,7 @@ function fileOpsReducer(state = initialState, action) {
case IMS_RECENT_COMICS_FETCHED:
return {
...state,
recentComics: action.data,
recentComics: action.data.docs,
};
case IMS_WANTED_COMICS_FETCHED:
return {
@@ -129,13 +146,7 @@ function fileOpsReducer(state = initialState, action) {
comicBookExtractionInProgress: true,
};
}
case IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS: {
return {
...state,
extractedComicBookArchive: action.extractedComicBookArchive,
comicBookExtractionInProgress: false,
};
}
case LOCATION_CHANGE: {
return {
...state,
@@ -146,23 +157,91 @@ function fileOpsReducer(state = initialState, action) {
case LS_IMPORT: {
return {
...state,
LSQueueImportStatus: "running",
};
}
case LS_COVER_EXTRACTED: {
console.log("BASH", action);
if (state.recentComics.length === 5) {
state.recentComics.pop();
}
return {
...state,
librarySearchResultCount: state.librarySearchResultCount + 1,
successfulJobCount: action.completedJobCount,
lastQueueJob: action.importResult.rawFileDetails.name,
recentComics: [...state.recentComics, action.importResult],
};
}
case LS_QUEUE_DRAINED: {
console.log("drained", action);
case LS_COVER_EXTRACTION_FAILED: {
return {
...state,
failedJobCount: action.failedJobCount,
};
}
case LS_IMPORT_QUEUE_DRAINED: {
localStorage.removeItem("sessionId");
return {
...state,
LSQueueImportStatus: "drained",
};
}
case RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION: {
console.log("Restoring state for an active import in progress...");
return {
...state,
successfulJobCount: action.completedJobCount,
failedJobCount: action.failedJobCount,
LSQueueImportStatus: action.queueStatus,
};
}
case LS_SET_QUEUE_STATUS: {
return {
...state,
LSQueueImportStatus: action.data.queueStatus,
};
}
case LS_IMPORT_JOB_STATISTICS_FETCHED: {
return {
...state,
importJobStatistics: action.data,
};
}
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_COMIC_ADDED: {
console.log("ADDED na anna", action);
return {
...state,
};
@@ -196,9 +275,8 @@ function fileOpsReducer(state = initialState, action) {
}
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
const foo = [];
console.log(action.data.hits)
if (!isUndefined(action.data.hits)) {
map(action.data.hits.hits, ({ _source }) => {
map(action.data.hits, ({ _source }) => {
foo.push(_source);
});
}
@@ -211,7 +289,7 @@ function fileOpsReducer(state = initialState, action) {
case WANTED_COMICS_FETCHED: {
const foo = [];
if (!isUndefined(action.data.hits)) {
map(action.data.hits.hits, ({ _source }) => {
map(action.data.hits, ({ _source }) => {
foo.push(_source);
});
}
@@ -221,7 +299,14 @@ function fileOpsReducer(state = initialState, action) {
SSCallInProgress: false,
};
}
case VOLUMES_FETCHED:
return {
...state,
volumes: action.data,
SSCallInProgress: false,
};
case SS_SEARCH_FAILED: {
return {
...state,
@@ -229,7 +314,12 @@ function fileOpsReducer(state = initialState, action) {
SSCallInProgress: false,
};
}
case LIBRARY_SERVICE_HEALTH: {
return {
...state,
libraryServiceStatus: action.status,
};
}
case FILEOPS_STATE_RESET: {
return {
...state,

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
const socketIOMiddleware = (socket) => {
return (store) => (next) => (action) => {
if (action.type === "EMIT_SOCKET_EVENT") {
const { event, data } = action.payload;
socket.emit(event, data);
}
return next(action);
};
};
export default socketIOMiddleware;

View File

@@ -0,0 +1,10 @@
import io from "socket.io-client";
import { SOCKET_BASE_URI } from "../../constants/endpoints";
const sessionId = localStorage.getItem("sessionId");
const socketIOConnectionInstance = io(SOCKET_BASE_URI, {
transports: ["websocket"],
withCredentials: true,
query: { sessionId },
});
export default socketIOConnectionInstance;

View File

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

View File

@@ -4,11 +4,11 @@ import { composeWithDevTools } from "@redux-devtools/extension";
import thunk from "redux-thunk";
import { createReduxHistoryContext } from "redux-first-history";
import { reducers } from "../reducers/index";
import { io } from "socket.io-client";
import socketIoMiddleware from "redux-socket.io-middleware";
import { SOCKET_BASE_URI } from "../constants/endpoints";
const socketConnection = io(SOCKET_BASE_URI, { transports: ["websocket"] });
import socketIOMiddleware from "../shared/middleware/SocketIOMiddleware";
import socketIOConnectionInstance from "../shared/socket.io/instance";
const customSocketIOMiddleware = socketIOMiddleware(socketIOConnectionInstance);
const { createReduxHistory, routerMiddleware, routerReducer } =
createReduxHistoryContext({
@@ -22,7 +22,8 @@ export const store = createStore(
}),
composeWithDevTools(
applyMiddleware(
socketIoMiddleware(socketConnection),
socketIoMiddleware(socketIOConnectionInstance),
customSocketIOMiddleware,
thunk,
routerMiddleware,
),

View File

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

View File

@@ -30,12 +30,12 @@ app.use(bodyParser.urlencoded({ extended: true }));
// REGISTER ROUTES
// all of the routes will be prefixed with /api
const routes: Router[] = Object.values(router);
app.use("/api", routes);
// app.use("/api", routes);
// Send index.html on root request
app.use(express.static("dist"));
app.use(express.static("public"));
// app.use(express.static("dist"));
// app.use(express.static("public"));
app.listen(port);
// app.listen(port);
console.log(`Server is listening on ${port}`);
// console.log(`Server is listening on ${port}`);

View File

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

23
vite.config.js Normal file
View File

@@ -0,0 +1,23 @@
import react from "@vitejs/plugin-react";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import { defineConfig } from "vite";
export default defineConfig({
publicDir: "public",
base: "",
build: "esnext",
server: { host: true },
plugins: [
nodeResolve({
// browser: true
exportConditions: ["node"],
}),
react({
// Use React plugin in all *.jsx and *.tsx files
include: "**/*.{jsx,tsx}",
babel: {
plugins: ["babel-plugin-styled-components"],
},
}),
],
});

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

17992
yarn.lock

File diff suppressed because it is too large Load Diff