Compare commits

...

32 Commits

Author SHA1 Message Date
9d95888628 Merge branch 'master' into import-queue-progress 2023-08-30 13:46:20 -04:00
284ccefc79 🔧 Added a console log for debugging purposes 2023-08-30 12:25:54 -04:00
874ba6b337 🔧 More finishing touches to Import UX 2023-08-29 23:57:47 -04:00
7efbbe0cfe 🧹 Cleaning up the tabulated job results 2023-08-29 00:10:22 -04:00
80b7113ed5 🔧 Wired up jobStatistics call 2023-08-28 23:57:01 -04:00
75777d32dd 🔢 Removed the useless LS_IMPORT event 2023-08-27 20:25:38 -04:00
ffa1b0cc09 🏗️ Scaffold for job results 2023-08-24 23:19:15 -04:00
084453b093 🪡 Fixed margin on the comicinfo.xml panel on the library page 2023-08-24 09:05:58 -05:00
9d5cca913e ⛑️ Removed useless deps 2023-08-23 21:53:54 -05:00
bab92fccc8 🔧 Fixed the airdcpp-apisocket version 2023-08-23 13:51:54 -05:00
b0282ccbb3 🔀 Bumped deps 2023-08-23 12:03:42 -05:00
a5c02d3fc6 🔧 Hardening the import UX 2023-08-23 11:48:13 -05:00
debd9a20bf 🐂 Queue controls 2023-08-22 22:07:33 -05:00
b4ef0b7817 🐂 Queue drain event reducer 2023-08-22 05:20:41 -04:00
0c2cbb6937 🐂 Queue pause/resume functionality 2023-08-21 17:55:26 -04:00
8c6890a044 📝 Added more details to import statuses 2023-08-18 23:37:35 -04:00
a2ae4ce79d 🔧 Refactoring the Import Page 2023-08-18 11:38:37 -04:00
31cd53b7a2 🧹 Minor change in socket.io connection code 2023-08-14 19:46:10 -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
11ef9e6e2a 🔧 Tooling for resumable socket.io sessions 2023-07-27 11:08:51 -07:00
Rishi Ghan
26fdb6d3a4 🐂 Support for session-tracking 2023-07-26 10:15:07 -07:00
a2f8d8b1e8 🐂 Support for showing import progress 2023-07-13 08:03:16 -07: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
ae9ec75983 🚥 Added service status panel scaffold 2023-06-20 11:32:16 -04: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
33 changed files with 5604 additions and 6211 deletions

View File

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

View File

@@ -1,10 +1,13 @@
import type { StorybookConfig } from "@storybook/react-vite"; import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [ addons: [
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions", "@storybook/addon-interactions",
"@storybook/addon-mdx-gfm"
], ],
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",

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

View File

@@ -40,9 +40,8 @@ For debugging and troubleshooting, you can run this app locally using these step
1. Clone this repo using `git clone https://github.com/rishighan/threetwo.git` 1. Clone this repo using `git clone https://github.com/rishighan/threetwo.git`
2. `yarn run dev` (you can ignore the warnings) 2. `yarn run dev` (you can ignore the warnings)
3. This will open `http://localhost:3050` in your default browser 3. This will open `http://localhost:5173` in your default browser
4. For testing `OPDS` functionality, create a folder called `comics` under `/src/server` and put some comics in there. The `OPDS` feed is accessed to `http://localhost:8050/api/opds` 4. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
5. Note that this is simply the UI layer and won't offer anything beyond a scaffold. You have to spin up the microservices locally to get it to work.
## Troubleshooting ## Troubleshooting

View File

@@ -7,7 +7,7 @@
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "rimraf dist && npm run build && vite", "dev": "rimraf dist && npm run build && vite",
"prod": "npm run build && vite", "start": "npm run build && vite",
"docs": "jsdoc -c jsdoc.json", "docs": "jsdoc -c jsdoc.json",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build"
@@ -15,7 +15,6 @@
"author": "Rishi Ghan", "author": "Rishi Ghan",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.13.17",
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git", "@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
"@dnd-kit/core": "^4.0.0", "@dnd-kit/core": "^4.0.0",
"@dnd-kit/sortable": "^5.0.0", "@dnd-kit/sortable": "^5.0.0",
@@ -23,14 +22,11 @@
"@fortawesome/fontawesome-free": "^6.3.0", "@fortawesome/fontawesome-free": "^6.3.0",
"@redux-devtools/extension": "^3.2.2", "@redux-devtools/extension": "^3.2.2",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",
"@tanstack/react-table": "^8.5.11", "@tanstack/react-table": "^8.9.3",
"@types/axios": "^0.14.0",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/socket.io": "^3.0.2",
"@types/socket.io-client": "^3.0.0",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",
"airdcpp-apisocket": "2.4.5-beta.1", "airdcpp-apisocket": "^2.5.0-beta.2",
"axios": "^1.3.4", "axios": "^1.3.4",
"axios-cache-interceptor": "^1.0.1", "axios-cache-interceptor": "^1.0.1",
"axios-rate-limit": "^1.3.0", "axios-rate-limit": "^1.3.0",
@@ -56,7 +52,7 @@
"react-collapsible": "^2.9.0", "react-collapsible": "^2.9.0",
"react-comic-viewer": "^0.4.0", "react-comic-viewer": "^0.4.0",
"react-day-picker": "^8.6.0", "react-day-picker": "^8.6.0",
"react-dom": "^18.1.0", "react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-final-form": "^6.5.9", "react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.4", "react-final-form-arrays": "^3.1.4",
@@ -78,27 +74,21 @@
"redux-thunk": "^2.4.2", "redux-thunk": "^2.4.2",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
"socket.io-client": "^4.3.2", "socket.io-client": "^4.3.2",
"styled-components": "^5.3.9", "styled-components": "^6.0.7",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"websocket": "^1.0.34", "websocket": "^1.0.34"
"xregexp": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.13.10", "@storybook/addon-essentials": "^7.3.2",
"@babel/core": "^7.13.10", "@storybook/addon-interactions": "^7.3.2",
"@babel/plugin-syntax-top-level-await": "^7.14.5", "@storybook/addon-links": "^7.3.2",
"@babel/plugin-transform-runtime": "^7.13.15", "@storybook/addon-mdx-gfm": "^7.3.2",
"@babel/preset-env": "^7.20.2", "@storybook/addon-onboarding": "^1.0.8",
"@babel/preset-react": "^7.18.6", "@storybook/blocks": "^7.3.2",
"@babel/preset-typescript": "^7.13.0", "@storybook/react": "^7.3.2",
"@storybook/addon-essentials": "^7.0.0-rc.3", "@storybook/react-vite": "^7.3.2",
"@storybook/addon-interactions": "^7.0.0-rc.3", "@storybook/testing-library": "^0.2.0",
"@storybook/addon-links": "^7.0.0-rc.3",
"@storybook/blocks": "^7.0.0-rc.3",
"@storybook/react": "^7.0.0-rc.3",
"@storybook/react-vite": "^7.0.0-rc.3",
"@storybook/testing-library": "^0.0.14-next.1",
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/ellipsize": "^0.1.1", "@types/ellipsize": "^0.1.1",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",
@@ -110,7 +100,7 @@
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0", "@typescript-eslint/parser": "^4.17.0",
"babel-eslint": "^10.0.0", "babel-plugin-styled-components": "^2.1.4",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bulma": "^0.9.4", "bulma": "^0.9.4",
"eslint": "^7.22.0", "eslint": "^7.22.0",
@@ -122,19 +112,20 @@
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.22.0",
"eslint-plugin-storybook": "^0.6.11", "eslint-plugin-storybook": "^0.6.13",
"express": "^4.17.1", "express": "^4.17.1",
"install": "^0.13.0", "install": "^0.13.0",
"jest": "^26.6.3", "jest": "^29.6.3",
"nodemon": "^1.17.3", "nodemon": "^3.0.1",
"npm": "^8.11.0", "npm": "^8.11.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"rimraf": "^4.1.3", "rimraf": "^4.1.3",
"sass": "^1.58.1", "sass": "^1.66.1",
"storybook": "^7.0.0-rc.3", "storybook": "^7.3.2",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "^5.0.2", "typescript": "^5.1.6",
"vite": "^4.2.0" "vite": "^4.4.9"
} }
} }

View File

@@ -1,7 +1,6 @@
import axios from "axios"; import axios from "axios";
import rateLimiter from "axios-rate-limit"; import rateLimiter from "axios-rate-limit";
import { setupCache } from "axios-cache-interceptor"; import { setupCache } from "axios-cache-interceptor";
import qs from "qs";
import { import {
CV_SEARCH_SUCCESS, CV_SEARCH_SUCCESS,
CV_API_CALL_IN_PROGRESS, CV_API_CALL_IN_PROGRESS,
@@ -34,7 +33,6 @@ export const getWeeklyPullList = (options) => async (dispatch) => {
dispatch({ dispatch({
type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS, type: CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
}); });
await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, { await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
method: "get", method: "get",
params: options, params: options,
@@ -64,9 +62,6 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
"Content-Type": "application/json", "Content-Type": "application/json",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
}, },
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: "repeat" });
},
}); });
switch (options.callURIAction) { switch (options.callURIAction) {
@@ -131,7 +126,7 @@ export const analyzeLibrary = (issues) => async (dispatch) => {
queryObjects, queryObjects,
}, },
}); });
dispatch({ dispatch({
type: CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED, type: CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED,
matches: foo.data, matches: foo.data,

View File

@@ -4,13 +4,12 @@ import {
COMICVINE_SERVICE_URI, COMICVINE_SERVICE_URI,
IMAGETRANSFORMATION_SERVICE_BASE_URI, IMAGETRANSFORMATION_SERVICE_BASE_URI,
LIBRARY_SERVICE_BASE_URI, LIBRARY_SERVICE_BASE_URI,
LIBRARY_SERVICE_HOST,
SEARCH_SERVICE_BASE_URI, SEARCH_SERVICE_BASE_URI,
JOB_QUEUE_SERVICE_BASE_URI,
} from "../constants/endpoints"; } from "../constants/endpoints";
import { import {
IMS_COMIC_BOOK_GROUPS_FETCHED, IMS_COMIC_BOOK_GROUPS_FETCHED,
IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS, IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS,
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
IMS_RECENT_COMICS_FETCHED, IMS_RECENT_COMICS_FETCHED,
IMS_WANTED_COMICS_FETCHED, IMS_WANTED_COMICS_FETCHED,
CV_API_CALL_IN_PROGRESS, CV_API_CALL_IN_PROGRESS,
@@ -22,23 +21,38 @@ import {
LS_IMPORT, LS_IMPORT,
IMG_ANALYSIS_CALL_IN_PROGRESS, IMG_ANALYSIS_CALL_IN_PROGRESS,
IMG_ANALYSIS_DATA_FETCH_SUCCESS, IMG_ANALYSIS_DATA_FETCH_SUCCESS,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS, IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
SS_SEARCH_RESULTS_FETCHED, SS_SEARCH_RESULTS_FETCHED,
SS_SEARCH_IN_PROGRESS, SS_SEARCH_IN_PROGRESS,
FILEOPS_STATE_RESET, FILEOPS_STATE_RESET,
LS_IMPORT_CALL_IN_PROGRESS, LS_IMPORT_CALL_IN_PROGRESS,
LS_TOGGLE_IMPORT_QUEUE,
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
WANTED_COMICS_FETCHED, WANTED_COMICS_FETCHED,
VOLUMES_FETCHED, VOLUMES_FETCHED,
CV_WEEKLY_PULLLIST_FETCHED, LIBRARY_SERVICE_HEALTH,
LS_SET_QUEUE_STATUS,
LS_IMPORT_JOB_STATISTICS_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { success } from "react-notification-system-redux"; import { success } from "react-notification-system-redux";
import { isNil, map } from "lodash"; import { isNil } from "lodash";
export const getServiceStatus = (serviceName?: string) => async (dispatch) => {
axios
.request({
url: `${LIBRARY_SERVICE_BASE_URI}/getHealthInformation`,
method: "GET",
transformResponse: (r: string) => JSON.parse(r),
})
.then((response) => {
const { data } = response;
dispatch({
type: LIBRARY_SERVICE_HEALTH,
status: data,
});
});
};
export async function walkFolder(path: string): Promise<Array<IFolderData>> { export async function walkFolder(path: string): Promise<Array<IFolderData>> {
return axios return axios
.request<Array<IFolderData>>({ .request<Array<IFolderData>>({
@@ -74,19 +88,38 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
// autoDismiss: 0, // autoDismiss: 0,
// }), // }),
// ); // );
const sessionId = localStorage.getItem("sessionId");
dispatch({ dispatch({
type: LS_IMPORT, type: LS_IMPORT,
meta: { remote: true }, });
data: {},
await axios.request({
url: `${LIBRARY_SERVICE_BASE_URI}/newImport`,
method: "POST",
data: { sessionId },
}); });
}; };
export const toggleImportQueueStatus = (options) => async (dispatch) => {
export const getImportJobResultStatistics = () => async (dispatch) => {
const result = await axios.request({
url: `${JOB_QUEUE_SERVICE_BASE_URI}/getJobResultStatistics`,
method: "GET",
});
dispatch({ dispatch({
type: LS_TOGGLE_IMPORT_QUEUE, type: LS_IMPORT_JOB_STATISTICS_FETCHED,
meta: { remote: true }, data: result.data,
data: { manjhul: "jigyadam", action: options.action },
}); });
}; };
export const setQueueControl =
(queueAction: string, queueStatus: string) => async (dispatch) => {
dispatch({
type: LS_SET_QUEUE_STATUS,
meta: { remote: true },
data: { queueAction, queueStatus },
});
};
/** /**
* Fetches comic book metadata for various types * Fetches comic book metadata for various types
* @return metadata for the comic book object categories * @return metadata for the comic book object categories
@@ -127,49 +160,50 @@ export const getComicBooks = (options) => async (dispatch) => {
* @returns Nothing. * @returns Nothing.
* @param payload * @param payload
*/ */
export const importToDB = (sourceName: string, metadata?: any) => (dispatch) => { export const importToDB =
try { (sourceName: string, metadata?: any) => (dispatch) => {
const comicBookMetadata = { try {
importType: "new", const comicBookMetadata = {
payload: { importType: "new",
rawFileDetails: { payload: {
name: "", rawFileDetails: {
}, name: "",
importStatus: {
isImported: true,
tagged: false,
matchedResult: {
score: "0",
}, },
importStatus: {
isImported: true,
tagged: false,
matchedResult: {
score: "0",
},
},
sourcedMetadata: metadata || null,
acquisition: { source: { wanted: true, name: sourceName } },
}, },
sourcedMetadata: metadata || null, };
acquisition: { source: { wanted: true, name: sourceName } }, dispatch({
} type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
};
dispatch({
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
});
return axios
.request({
url: `${LIBRARY_SERVICE_BASE_URI}/rawImportToDb`,
method: "POST",
data: comicBookMetadata,
// transformResponse: (r: string) => JSON.parse(r),
})
.then((response) => {
const { data } = response;
dispatch({
type: IMS_CV_METADATA_IMPORT_SUCCESSFUL,
importResult: data,
});
}); });
} catch (error) { return axios
dispatch({ .request({
type: IMS_CV_METADATA_IMPORT_FAILED, url: `${LIBRARY_SERVICE_BASE_URI}/rawImportToDb`,
importError: error, method: "POST",
}); data: comicBookMetadata,
} // transformResponse: (r: string) => JSON.parse(r),
}; })
.then((response) => {
const { data } = response;
dispatch({
type: IMS_CV_METADATA_IMPORT_SUCCESSFUL,
importResult: data,
});
});
} catch (error) {
dispatch({
type: IMS_CV_METADATA_IMPORT_FAILED,
importError: error,
});
}
};
export const fetchVolumeGroups = () => async (dispatch) => { export const fetchVolumeGroups = () => async (dispatch) => {
try { try {
@@ -254,24 +288,23 @@ export const fetchComicVineMatches =
* @returns {any} * @returns {any}
*/ */
export const extractComicArchive = export const extractComicArchive =
(path: string, options: any): any => (path: string, options: any): any =>
async (dispatch) => { async (dispatch) => {
dispatch({ dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS, type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
}); });
await axios({ await axios({
method: "POST", method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`, url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
}, },
data: { data: {
filePath: path, filePath: path,
options, options,
}, },
}); });
}; };
/** /**
* Description * Description
@@ -349,4 +382,4 @@ export const analyzeImage =
type: IMG_ANALYSIS_DATA_FETCH_SUCCESS, type: IMG_ANALYSIS_DATA_FETCH_SUCCESS,
result: foo.data, result: foo.data,
}); });
}; };

View File

@@ -15,14 +15,15 @@ export const AirDCPPSettingsForm = (): ReactElement => {
try { try {
if (!isUndefined(hostname)) { if (!isUndefined(hostname)) {
const matches = hostname.match(hostnameRegex); const matches = hostname.match(hostnameRegex);
return (isNil(matches) && matches.length !== 0) ? hostname : "Invalid hostname; it should not contain special characters"; return isNil(matches) && matches.length !== 0
? hostname
: "Invalid hostname; it should not contain special characters";
} }
} } catch {
catch {
return null; return null;
} }
} };
const onSubmit = useCallback(async (values) => { const onSubmit = useCallback(async (values) => {
try { try {
airDCPPSettings.setSettings(values); airDCPPSettings.setSettings(values);
@@ -39,7 +40,7 @@ export const AirDCPPSettingsForm = (): ReactElement => {
airDCPPSettings.setSettings({}); airDCPPSettings.setSettings({});
dispatch(deleteSettings()); dispatch(deleteSettings());
}, []); }, []);
const validate = async () => { }; const validate = async () => {};
const initFormData = !isUndefined( const initFormData = !isUndefined(
airDCPPSettings.airDCPPState.settings.directConnect, airDCPPSettings.airDCPPState.settings.directConnect,
) )
@@ -67,13 +68,20 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</span> </span>
</p> </p>
<div className="control is-expanded"> <div className="control is-expanded">
<Field <Field name="hostname" validate={hostValidator}>
name="hostname"
validate={hostValidator}>
{({ input, meta }) => ( {({ input, meta }) => (
<div> <div>
<input {...input} type="text" placeholder="AirDC++ hostname" className="input" /> <input
{meta.error && meta.touched && <span className="is-size-7 has-text-danger">{meta.error}</span>} {...input}
type="text"
placeholder="AirDC++ hostname"
className="input"
/>
{meta.error && meta.touched && (
<span className="is-size-7 has-text-danger">
{meta.error}
</span>
)}
</div> </div>
)} )}
</Field> </Field>

View File

@@ -1,6 +1,5 @@
import React, { ReactElement, useContext, useEffect } from "react"; import React, { ReactElement, useContext, useEffect } from "react";
import Dashboard from "./Dashboard/Dashboard"; import Dashboard from "./Dashboard/Dashboard";
import Import from "./Import"; import Import from "./Import";
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer"; import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
import TabulatedContentContainer from "./Library/TabulatedContentContainer"; import TabulatedContentContainer from "./Library/TabulatedContentContainer";
@@ -17,7 +16,9 @@ import {
AirDCPPSocketContextProvider, AirDCPPSocketContextProvider,
AirDCPPSocketContext, AirDCPPSocketContext,
} from "../context/AirDCPPSocket"; } from "../context/AirDCPPSocket";
import { isEmpty, isUndefined } from "lodash"; import { SocketIOProvider } from "../context/SocketIOContext";
import socketIOConnectionInstance from "../shared/socket.io/instance";
import { isEmpty, isNil, isUndefined } from "lodash";
import { import {
AIRDCPP_DOWNLOAD_PROGRESS_TICK, AIRDCPP_DOWNLOAD_PROGRESS_TICK,
LS_SINGLE_IMPORT, LS_SINGLE_IMPORT,
@@ -95,45 +96,65 @@ const AirDCPPSocketComponent = (): ReactElement => {
return <></>; return <></>;
}; };
export const App = (): ReactElement => { 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 ( return (
<AirDCPPSocketContextProvider> <SocketIOProvider socket={socketIOConnectionInstance}>
<div> <AirDCPPSocketContextProvider>
<AirDCPPSocketComponent /> <div>
<Navbar /> <AirDCPPSocketComponent />
<Routes> <Navbar />
<Route path="/" element={<Dashboard />} /> <Routes>
<Route path="/import" element={<Import path={"./comics"} />} /> <Route path="/" element={<Dashboard />} />
<Route <Route path="/import" element={<Import path={"./comics"} />} />
path="/library" <Route
element={<TabulatedContentContainer category="library" />} path="/library"
/> element={<TabulatedContentContainer category="library" />}
<Route path="/library-grid" element={<LibraryGrid />} /> />
<Route path="/downloads" element={<Downloads data={{}} />} /> <Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/search" element={<Search />} /> <Route path="/downloads" element={<Downloads data={{}} />} />
<Route <Route path="/search" element={<Search />} />
path={"/comic/details/:comicObjectId"} <Route
element={<ComicDetailContainer />} path={"/comic/details/:comicObjectId"}
/> element={<ComicDetailContainer />}
<Route />
path={"/volume/details/:comicObjectId"} <Route
element={<VolumeDetail />} path={"/volume/details/:comicObjectId"}
/> element={<VolumeDetail />}
<Route path="/settings" element={<Settings />} /> />
<Route <Route path="/settings" element={<Settings />} />
path="/pull-list/all" <Route
element={<TabulatedContentContainer category="pullList" />} path="/pull-list/all"
/> element={<TabulatedContentContainer category="pullList" />}
<Route />
path="/wanted/all" <Route
element={<TabulatedContentContainer category="wanted" />} path="/wanted/all"
/> element={<TabulatedContentContainer category="wanted" />}
<Route />
path="/volumes/all" <Route
element={<TabulatedContentContainer category="volumes" />} path="/volumes/all"
/> element={<TabulatedContentContainer category="volumes" />}
</Routes> />
</div> </Routes>
</AirDCPPSocketContextProvider> </div>
</AirDCPPSocketContextProvider>
</SocketIOProvider>
); );
}; };

View File

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

View File

@@ -12,6 +12,7 @@ import {
} from "../../actions/fileops.actions"; } from "../../actions/fileops.actions";
import { getLibraryStatistics } from "../../actions/comicinfo.actions"; import { getLibraryStatistics } from "../../actions/comicinfo.actions";
import { isEmpty, isNil } from "lodash"; import { isEmpty, isNil } from "lodash";
import Header from "../Header";
export const Dashboard = (): ReactElement => { export const Dashboard = (): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -43,7 +44,7 @@ export const Dashboard = (): ReactElement => {
}, []); }, []);
const recentComics = useSelector( const recentComics = useSelector(
(state: RootState) => state.fileOps.recentComics (state: RootState) => state.fileOps.recentComics,
); );
const wantedComics = useSelector( const wantedComics = useSelector(
(state: RootState) => state.fileOps.wantedComics, (state: RootState) => state.fileOps.wantedComics,
@@ -64,6 +65,36 @@ export const Dashboard = (): ReactElement => {
<> <>
{/* Pull List */} {/* Pull List */}
<PullList issues={recentComics} /> <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 */} {/* Stats */}
{!isEmpty(libraryStatistics) && ( {!isEmpty(libraryStatistics) && (
@@ -74,8 +105,8 @@ export const Dashboard = (): ReactElement => {
<WantedComicsList comics={wantedComics} /> <WantedComicsList comics={wantedComics} />
)} )}
{/* Recent imports */} {/* Recent imports */}
<RecentlyImported comicBookCovers={recentComics} /> <RecentlyImported comicBookCovers={recentComics} />
{/* Volumes */} {/* Volumes */}
{!isEmpty(volumeGroups) && ( {!isEmpty(volumeGroups) && (
<VolumeGroups volumeGroups={volumeGroups} /> <VolumeGroups volumeGroups={volumeGroups} />
@@ -94,4 +125,4 @@ export const Dashboard = (): ReactElement => {
); );
}; };
export default Dashboard; export default Dashboard;

View File

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

View File

@@ -125,7 +125,7 @@ export const RecentlyImported = ({
<dd className="is-size-9"> <dd className="is-size-9">
<dl> <dl>
<span className="icon custom-icon"> <span className="icon custom-icon">
<img src={`/img/${icon}`} /> <img src={`/src/client/assets/img/${icon}`} />
</span> </span>
</dl> </dl>
<dl> <dl>

View File

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

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 { useSelector, useDispatch } from "react-redux";
import { import {
fetchComicBookMetadata, fetchComicBookMetadata,
toggleImportQueueStatus, getImportJobResultStatistics,
setQueueControl,
} from "../actions/fileops.actions"; } from "../actions/fileops.actions";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import { format } from "date-fns";
import Loader from "react-loader-spinner"; import Loader from "react-loader-spinner";
import { isEmpty, isNil, isUndefined } from "lodash";
interface IProps { interface IProps {
matches?: unknown; matches?: unknown;
@@ -29,102 +32,203 @@ interface IProps {
export const Import = (props: IProps): ReactElement => { export const Import = (props: IProps): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const libraryQueueResults = useSelector( const successfulImportJobCount = useSelector(
(state: RootState) => state.fileOps.librarySearchResultCount, (state: RootState) => state.fileOps.successfulJobCount,
);
const failedImportJobCount = useSelector(
(state: RootState) => state.fileOps.failedJobCount,
);
const lastQueueJob = useSelector(
(state: RootState) => state.fileOps.lastQueueJob,
); );
const libraryQueueImportStatus = useSelector( 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(() => { const initiateImport = useCallback(() => {
if (typeof props.path !== "undefined") { if (typeof props.path !== "undefined") {
dispatch(fetchComicBookMetadata(props.path)); dispatch(fetchComicBookMetadata(props.path));
} }
}, [dispatch]); }, [dispatch]);
const toggleImport = useCallback(() => { const toggleQueue = useCallback(
setImportQueueStatus(!isImportQueuePaused); (queueAction: string, queueStatus: string) => {
if (isImportQueuePaused === false) { dispatch(setQueueControl(queueAction, queueStatus));
dispatch(toggleImportQueueStatus({ action: "resume" })); },
} else if (isImportQueuePaused === true) { [],
dispatch(toggleImportQueueStatus({ action: "pause" })); );
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 ( return (
<div className="container"> <div className="container">
<section className="section is-small"> <section className="section is-small">
<h1 className="title">Import</h1> <h1 className="title">Import Comics</h1>
<article className="message is-dark"> <article className="message is-dark">
<div className="message-body"> <div className="message-body">
<p className="mb-2"> <p className="mb-2">
<span className="tag is-medium is-info is-light"> <span className="tag is-medium is-info is-light">
Import Only Import Comics
</span> </span>
will add comics identified from the mapped folder into the local will add comics identified from the mapped folder into ThreeTwo's
db. database.
</p> </p>
<p> <p>
<span className="tag is-medium is-info is-light"> Metadata from ComicInfo.xml, if present, will also be extracted.
Import and Tag </p>
</span> <p>
will scan the ComicVine, shortboxed APIs and import comics from This process could take a while, if you have a lot of comics, or
the mapped folder with the additional metadata. are importing over a network connection.
</p> </p>
</div> </div>
</article> </article>
<p className="buttons"> <p className="buttons">
<button <button
className={ className={
libraryQueueImportStatus libraryQueueImportStatus === "drained" ||
? "button is-loading is-medium" libraryQueueImportStatus === undefined
: "button is-medium" ? "button is-medium"
: "button is-loading is-medium"
} }
onClick={initiateImport} onClick={initiateImport}
> >
<span className="icon"> <span className="icon">
<i className="fas fa-file-import"></i> <i className="fas fa-file-import"></i>
</span> </span>
<span>Import Only</span> <span>Start Import</span>
</button>
<button className="button is-medium">
<span className="icon">
<i className="fas fa-tag"></i>
</span>
<span>Import and Tag</span>
</button> </button>
</p> </p>
<div className="columns is-multiline"> {libraryQueueImportStatus !== "drained" &&
<div className="column is-one-fifth"> !isUndefined(libraryQueueImportStatus) && (
<div className="box control-palette"> <>
<span className="is-size-2 has-text-weight-bold"> <table className="table">
{JSON.stringify(libraryQueueResults, null, 2)} <thead>
</span> <tr>
</div> <th>Completed Jobs</th>
<div className="is-half"> <th>Failed Jobs</th>
<div className="content"> <th>Queue Controls</th>
<div className="control"> <th>Queue Status</th>
<button </tr>
className="button is-warning is-light" </thead>
onClick={toggleImport}
> <tbody>
{!isImportQueuePaused ? pauseIconText : playIconText} <tr>
</button> <th>
</div> {successfulImportJobCount > 0 && (
</div> <div className="box has-background-success-light has-text-centered">
</div> <span className="is-size-2 has-text-weight-bold">
</div> {successfulImportJobCount}
</div> </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> </section>
</div> </div>
); );

View File

@@ -47,7 +47,7 @@ export const Library = (): ReactElement => {
const ComicInfoXML = (value) => { const ComicInfoXML = (value) => {
return value.data ? ( return value.data ? (
<div className="comicvine-metadata"> <div className="comicvine-metadata mt-3">
<dl> <dl>
<span className="tags has-addons is-size-7"> <span className="tags has-addons is-size-7">
<span className="tag">Series</span> <span className="tag">Series</span>

View File

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

View File

@@ -20,6 +20,25 @@ const Navbar: React.FunctionComponent = (props) => {
const socketDisconnectionReason = useSelector( const socketDisconnectionReason = useSelector(
(state: RootState) => state.airdcpp.socketDisconnectionReason, (state: RootState) => state.airdcpp.socketDisconnectionReason,
); );
// Import-related selector hooks
const successfulImportJobCount = useSelector(
(state: RootState) => state.fileOps.successfulJobCount,
);
const failedImportJobCount = useSelector(
(state: RootState) => state.fileOps.failedJobCount,
);
const lastQueueJob = useSelector(
(state: RootState) => state.fileOps.lastQueueJob,
);
const libraryQueueImportStatus = useSelector(
(state: RootState) => state.fileOps.LSQueueImportStatus,
);
const allImportJobResults = useSelector(
(state: RootState) => state.fileOps.importJobStatistics,
);
return ( return (
<nav className="navbar is-fixed-top"> <nav className="navbar is-fixed-top">
<div className="navbar-brand"> <div className="navbar-brand">
@@ -85,12 +104,46 @@ const Navbar: React.FunctionComponent = (props) => {
{downloadProgressTick && <div className="pulsating-circle"></div>} {downloadProgressTick && <div className="pulsating-circle"></div>}
</a> </a>
{!isUndefined(downloadProgressTick) ? ( {!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown is-right"> <div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item"> <a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} /> <DownloadProgressTick data={downloadProgressTick} />
</a> </div> </a>
</div>
) : null} ) : null}
</div> </div>
{!isUndefined(libraryQueueImportStatus) &&
location.hash !== "#/import" ? (
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-file-import has-text-warning-dark"></i>
</a>
<div className="navbar-dropdown is-right is-boxed">
<a className="navbar-item">
<ul>
{successfulImportJobCount > 0 ? (
<li className="mb-2">
<span className="tag is-success mr-2">
{successfulImportJobCount}
</span>
imported.
</li>
) : null}
{failedImportJobCount > 0 ? (
<li>
<span className="tag is-danger mr-2">
{failedImportJobCount}
</span>
failed to import.
</li>
) : null}
</ul>
</a>
</div>
</div>
) : null}
{/* AirDC++ socket connection status */} {/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable"> <div className="navbar-item has-dropdown is-hoverable">
{airDCPPSocketConnectionStatus ? ( {airDCPPSocketConnectionStatus ? (
@@ -98,7 +151,7 @@ const Navbar: React.FunctionComponent = (props) => {
<a className="navbar-link is-arrowless has-text-success"> <a className="navbar-link is-arrowless has-text-success">
<i className="fa-solid fa-bolt"></i> <i className="fa-solid fa-bolt"></i>
</a> </a>
<div className="navbar-dropdown mt-3 pt-4 pr-2 pl-2 is-right airdcpp-status is-boxed"> <div className="navbar-dropdown pr-2 pl-2 is-right airdcpp-status is-boxed">
{/* AirDC++ Session Information */} {/* AirDC++ Session Information */}
<p> <p>

View File

@@ -15,7 +15,7 @@ export const PullList = (): ReactElement => {
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2022-11-15", startDate: "2023-7-28",
pageSize: "15", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),
@@ -109,15 +109,15 @@ export const PullList = (): ReactElement => {
{!isNil(pullListComics) && ( {!isNil(pullListComics) && (
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table
sourceData={pullListComics} sourceData={pullListComics}
columns={columnData} columns={columnData}
totalPages={pullListComics.length} totalPages={pullListComics.length}
paginationHandlers={{ paginationHandlers={{
nextPage: nextPageHandler, nextPage: nextPageHandler,
previousPage: previousPageHandler, previousPage: previousPageHandler,
}} }}
/> />
</div> </div>
</div> </div>
)} )}

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 { isNil, isEmpty } from "lodash";
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings"; import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
import { importToDB } from "../actions/fileops.actions"; import { importToDB } from "../actions/fileops.actions";

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

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"; export const LS_IMPORT_CALL_IN_PROGRESS = "LS_IMPORT_CALL_IN_PROGRESS";
// Library import bull mq queue control // Library import bull mq queue control
export const LS_TOGGLE_IMPORT_QUEUE = "LS_TOGGLE_IMPORT_QUEUE"; export const LS_TOGGLE_IMPORT_QUEUE = "LS_TOGGLE_IMPORT_QUEUE";
export const LS_QUEUE_DRAINED = "LS_QUEUE_DRAINED";
// ComicVine Metadata // ComicVine Metadata
export const IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS = export const IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS =
@@ -51,11 +50,12 @@ export const IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS =
"IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS"; "IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS";
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED = export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED"; "IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
export const VOLUMES_FETCHED="VOLUMES_FETCHED"; export const VOLUMES_FETCHED = "VOLUMES_FETCHED";
// search results from the Search service // search results from the Search service
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED"; export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";
export const SS_SEARCH_RESULTS_FETCHED_SPECIAL = "SS_SEARCH_RESULTS_FETCHED_SPECIAL"; export const SS_SEARCH_RESULTS_FETCHED_SPECIAL =
"SS_SEARCH_RESULTS_FETCHED_SPECIAL";
export const SS_SEARCH_IN_PROGRESS = "SS_SEARCH_IN_PROGRESS"; export const SS_SEARCH_IN_PROGRESS = "SS_SEARCH_IN_PROGRESS";
export const SS_SEARCH_FAILED = "SS_SEARCH_FAILED"; export const SS_SEARCH_FAILED = "SS_SEARCH_FAILED";
@@ -105,7 +105,7 @@ export const AIRDCPP_SEARCH_RESULTS_UPDATED = "AIRDCPP_SEARCH_RESULTS_UPDATED";
export const AIRDCPP_SEARCH_COMPLETE = "AIRDCPP_SEARCH_COMPLETE"; export const AIRDCPP_SEARCH_COMPLETE = "AIRDCPP_SEARCH_COMPLETE";
// AirDC++ related library query for issues with bundles associated with them // AirDC++ related library query for issues with bundles associated with them
export const LIBRARY_ISSUE_BUNDLES = "LIBRARY_ISSUE_BUNDLES"; export const LIBRARY_ISSUE_BUNDLES = "LIBRARY_ISSUE_BUNDLES";
export const AIRDCPP_HUB_SEARCHES_SENT = "AIRDCPP_HUB_SEARCHES_SENT"; export const AIRDCPP_HUB_SEARCHES_SENT = "AIRDCPP_HUB_SEARCHES_SENT";
export const AIRDCPP_RESULT_DOWNLOAD_INITIATED = export const AIRDCPP_RESULT_DOWNLOAD_INITIATED =
@@ -124,10 +124,17 @@ export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";
// Comics marked as "wanted" // Comics marked as "wanted"
export const WANTED_COMICS_FETCHED = "WANTED_COMICS_FETCHED"; export const WANTED_COMICS_FETCHED = "WANTED_COMICS_FETCHED";
// LIBRARY SOCKET ENDPOINT // LIBRARY Service import queue-related action types
export const LS_IMPORT = "LS_IMPORT"; export const LS_IMPORT = "LS_IMPORT";
export const LS_COVER_EXTRACTED = "LS_COVER_EXTRACTED"; export const LS_COVER_EXTRACTED = "LS_COVER_EXTRACTED";
export const LS_COVER_EXTRACTION_FAILED = "LS_COVER_EXTRACTION_FAILED";
export const LS_COMIC_ADDED = "LS_COMIC_ADDED"; export const LS_COMIC_ADDED = "LS_COMIC_ADDED";
export const LS_IMPORT_QUEUE_DRAINED = "LS_IMPORT_QUEUE_DRAINED";
export const LS_SET_QUEUE_STATUS = "LS_SET_QUEUE_STATUS";
export const RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION =
"RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION";
export const LS_IMPORT_JOB_STATISTICS_FETCHED =
"LS_IMPORT_JOB_STATISTICS_FETCHED";
// Settings // Settings
export const SETTINGS_CALL_IN_PROGRESS = "SETTINGS_CALL_IN_PROGRESS"; export const SETTINGS_CALL_IN_PROGRESS = "SETTINGS_CALL_IN_PROGRESS";
@@ -139,4 +146,7 @@ export const SETTINGS_DB_FLUSH_SUCCESS = "SETTINGS_DB_FLUSH_SUCCESS";
// Metron Metadata // Metron Metadata
export const METRON_DATA_FETCH_SUCCESS = "METRON_DATA_FETCH_SUCCESS"; export const METRON_DATA_FETCH_SUCCESS = "METRON_DATA_FETCH_SUCCESS";
export const METRON_DATA_FETCH_IN_PROGRESS = "METRON_DATA_FETCH_IN_PROGRESS"; export const METRON_DATA_FETCH_IN_PROGRESS = "METRON_DATA_FETCH_IN_PROGRESS";
export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR"; export const METRON_DATA_FETCH_ERROR = "METRON_DATA_FETCH_ERROR";
// 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 options.apiPath
); );
}; };
console.log(import.meta);
export const CORS_PROXY_SERVER_URI = hostURIBuilder({ export const CORS_PROXY_SERVER_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "8050", port: "8050",
apiPath: "/", apiPath: "/",
}); });
export const COMICVINE_SERVICE_URI = hostURIBuilder({ export const COMICVINE_SERVICE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3080", port: "3080",
apiPath: "/api/comicvine", apiPath: "/api/comicvine",
}); });
export const METRON_SERVICE_URI = hostURIBuilder({ export const METRON_SERVICE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3080", port: "3080",
apiPath: "/api/metron", apiPath: "/api/metron",
}); });
export const API_BASE_URI = hostURIBuilder({ export const API_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "8050", port: "8050",
apiPath: "/api", apiPath: "/api",
}); });
export const LIBRARY_SERVICE_HOST = hostURIBuilder({ export const LIBRARY_SERVICE_HOST = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: ``, apiPath: ``,
}); });
export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({ export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/library", apiPath: "/api/library",
}); });
export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({ export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/search", apiPath: "/api/search",
}); });
export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({ export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/settings", apiPath: "/api/settings",
}); });
export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({ export const IMAGETRANSFORMATION_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3000", port: "3000",
apiPath: "/api/imagetransformation", apiPath: "/api/imagetransformation",
}); });
export const SOCKET_BASE_URI = hostURIBuilder({ export const SOCKET_BASE_URI = hostURIBuilder({
protocol: "ws", protocol: "ws",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
port: "3001", port: "3001",
apiPath: `/`, apiPath: `/`,
}); });
export const JOB_QUEUE_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http",
host: import.meta.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000",
apiPath: `/api/jobqueue`,
});

View File

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

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

@@ -15,10 +15,9 @@ import {
IMS_COMIC_BOOK_GROUPS_FETCHED, IMS_COMIC_BOOK_GROUPS_FETCHED,
IMS_COMIC_BOOK_GROUPS_CALL_FAILED, IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS, IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
LS_IMPORT, LS_IMPORT,
LS_COVER_EXTRACTED, LS_COVER_EXTRACTED,
LS_QUEUE_DRAINED, LS_COVER_EXTRACTION_FAILED,
LS_COMIC_ADDED, LS_COMIC_ADDED,
IMG_ANALYSIS_CALL_IN_PROGRESS, IMG_ANALYSIS_CALL_IN_PROGRESS,
IMG_ANALYSIS_DATA_FETCH_SUCCESS, IMG_ANALYSIS_DATA_FETCH_SUCCESS,
@@ -30,6 +29,11 @@ import {
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
VOLUMES_FETCHED, VOLUMES_FETCHED,
COMICBOOK_EXTRACTION_SUCCESS, COMICBOOK_EXTRACTION_SUCCESS,
LIBRARY_SERVICE_HEALTH,
LS_IMPORT_QUEUE_DRAINED,
LS_SET_QUEUE_STATUS,
RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION,
LS_IMPORT_JOB_STATISTICS_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils"; import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints"; import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
@@ -40,6 +44,7 @@ const initialState = {
SSCallInProgress: false, SSCallInProgress: false,
imageAnalysisResults: {}, imageAnalysisResults: {},
comicBookExtractionInProgress: false, comicBookExtractionInProgress: false,
LSQueueImportStatus: undefined,
comicBookMetadata: [], comicBookMetadata: [],
comicVolumeGroups: [], comicVolumeGroups: [],
isSocketConnected: false, isSocketConnected: false,
@@ -55,11 +60,14 @@ const initialState = {
libraryComics: [], libraryComics: [],
volumes: [], volumes: [],
librarySearchResultsFormatted: [], librarySearchResultsFormatted: [],
librarySearchResultCount: 0, lastQueueJob: "",
successfulJobCount: 0,
failedJobCount: 0,
importJobStatistics: [],
libraryQueueResults: [], libraryQueueResults: [],
librarySearchError: {}, librarySearchError: {},
libraryServiceStatus: {},
}; };
function fileOpsReducer(state = initialState, action) { function fileOpsReducer(state = initialState, action) {
switch (action.type) { switch (action.type) {
case IMS_COMICBOOK_METADATA_FETCHED: case IMS_COMICBOOK_METADATA_FETCHED:
@@ -149,17 +157,58 @@ function fileOpsReducer(state = initialState, action) {
case LS_IMPORT: { case LS_IMPORT: {
return { return {
...state, ...state,
LSQueueImportStatus: "running",
}; };
} }
case LS_COVER_EXTRACTED: { case LS_COVER_EXTRACTED: {
console.log("BASH", action); if (state.recentComics.length === 5) {
if(state.recentComics.length === 5) {
state.recentComics.pop(); state.recentComics.pop();
} }
return { return {
...state, ...state,
librarySearchResultCount: state.librarySearchResultCount + 1, successfulJobCount: action.completedJobCount,
recentComics: [...state.recentComics, action.result.data.importResult] lastQueueJob: action.importResult.rawFileDetails.name,
recentComics: [...state.recentComics, action.importResult],
};
}
case LS_COVER_EXTRACTION_FAILED: {
return {
...state,
failedJobCount: action.failedJobCount,
};
}
case LS_IMPORT_QUEUE_DRAINED: {
localStorage.removeItem("sessionId");
return {
...state,
LSQueueImportStatus: "drained",
};
}
case RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION: {
console.log("Restoring state for an active import in progress...");
return {
...state,
successfulJobCount: action.completedJobCount,
failedJobCount: action.failedJobCount,
LSQueueImportStatus: action.queueStatus,
};
}
case LS_SET_QUEUE_STATUS: {
return {
...state,
LSQueueImportStatus: action.data.queueStatus,
};
}
case LS_IMPORT_JOB_STATISTICS_FETCHED: {
return {
...state,
importJobStatistics: action.data,
}; };
} }
@@ -191,14 +240,8 @@ function fileOpsReducer(state = initialState, action) {
}; };
} }
} }
case LS_QUEUE_DRAINED: {
console.log("drained", action);
return {
...state,
};
}
case LS_COMIC_ADDED: { case LS_COMIC_ADDED: {
console.log("ADDED na anna", action);
return { return {
...state, ...state,
}; };
@@ -271,7 +314,12 @@ function fileOpsReducer(state = initialState, action) {
SSCallInProgress: false, SSCallInProgress: false,
}; };
} }
case LIBRARY_SERVICE_HEALTH: {
return {
...state,
libraryServiceStatus: action.status,
};
}
case FILEOPS_STATE_RESET: { case FILEOPS_STATE_RESET: {
return { return {
...state, ...state,
@@ -283,4 +331,4 @@ function fileOpsReducer(state = initialState, action) {
} }
} }
export default fileOpsReducer; export default fileOpsReducer;

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

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

View File

@@ -6,6 +6,7 @@ export default defineConfig({
publicDir: "public", publicDir: "public",
base: "", base: "",
build: "esnext", build: "esnext",
server: { host: true },
plugins: [ plugins: [
nodeResolve({ nodeResolve({
// browser: true // browser: true

10692
yarn.lock

File diff suppressed because it is too large Load Diff