Compare commits

...

39 Commits

Author SHA1 Message Date
33f67e99f8 🔧 Fixed the path to funding.yml 2023-01-28 09:31:07 -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
45 changed files with 1155 additions and 656 deletions

BIN
ComicVine Matching.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
DC++ integration.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

BIN
Dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
Library.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

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) [![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://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/Dashboard.png)
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/Library.png)
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/DC%2B%2B%20integration.png)
![](https://raw.githubusercontent.com/rishighan/threetwo/rishighan-screenshots-dec-2022/ComicVine%20Matching.png)
### 🦄 Early Development Support Channel ### 🦄 Early Development Support Channel

1
funding.yml Normal file
View File

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

View File

@@ -44,10 +44,6 @@
"babel-preset-minify": "^0.5.2", "babel-preset-minify": "^0.5.2",
"better-docs": "^2.7.2", "better-docs": "^2.7.2",
"comlink-loader": "^2.0.0", "comlink-loader": "^2.0.0",
"compromise": "^13.11.3",
"compromise-dates": "^2.2.1",
"compromise-numbers": "^1.4.0",
"compromise-sentences": "^0.3.0",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"ellipsize": "^0.1.0", "ellipsize": "^0.1.0",
@@ -63,13 +59,13 @@
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"qs": "^6.10.5", "qs": "^6.10.5",
"react": "^18.1.0", "react": "^18.2.0",
"react-collapsible": "^2.9.0", "react-collapsible": "^2.9.0",
"react-comic-viewer": "^0.3.5", "react-comic-viewer": "^0.4.0",
"react-day-picker": "^8.0.6", "react-day-picker": "^8.0.6",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.3", "react-final-form-arrays": "^3.1.3",
"react-loader-spinner": "^4.0.0", "react-loader-spinner": "^4.0.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
@@ -138,7 +134,6 @@
"bulma": "^0.9.3", "bulma": "^0.9.3",
"clean-webpack-plugin": "^1.0.0", "clean-webpack-plugin": "^1.0.0",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"compromise-strict": "^0.0.2",
"concurrently": "^4.0.0", "concurrently": "^4.0.0",
"copy-webpack-plugin": "^9.0.1", "copy-webpack-plugin": "^9.0.1",
"css-loader": "^5.1.2", "css-loader": "^5.1.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

@@ -32,9 +32,11 @@ import {
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
WANTED_COMICS_FETCHED, WANTED_COMICS_FETCHED,
VOLUMES_FETCHED,
CV_WEEKLY_PULLLIST_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { success } from "react-notification-system-redux"; import { success } from "react-notification-system-redux";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { isNil, map } from "lodash"; import { isNil, map } from "lodash";
export async function walkFolder(path: string): Promise<Array<IFolderData>> { export async function walkFolder(path: string): Promise<Array<IFolderData>> {
@@ -58,15 +60,6 @@ export async function walkFolder(path: string): Promise<Array<IFolderData>> {
* @return the comic book metadata * @return the comic book metadata
*/ */
export const fetchComicBookMetadata = () => async (dispatch) => { export const fetchComicBookMetadata = () => async (dispatch) => {
const extractionOptions = {
extractTarget: "cover",
targetExtractionFolder: "./userdata/covers",
extractionMode: "bulk",
paginationOptions: {
pageLimit: 25,
page: 1,
},
};
dispatch({ dispatch({
type: LS_IMPORT_CALL_IN_PROGRESS, type: LS_IMPORT_CALL_IN_PROGRESS,
}); });
@@ -84,7 +77,7 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
dispatch({ dispatch({
type: LS_IMPORT, type: LS_IMPORT,
meta: { remote: true }, meta: { remote: true },
data: { extractionOptions }, data: {},
}); });
}; };
export const toggleImportQueueStatus = (options) => async (dispatch) => { export const toggleImportQueueStatus = (options) => async (dispatch) => {
@@ -134,21 +127,24 @@ export const getComicBooks = (options) => async (dispatch) => {
* @returns Nothing. * @returns Nothing.
* @param payload * @param payload
*/ */
export const importToDB = (sourceName: string, payload?: any) => (dispatch) => { export const importToDB = (sourceName: string, metadata?: any) => (dispatch) => {
try { try {
const comicBookMetadata = { const comicBookMetadata = {
rawFileDetails: { importType: "new",
name: "", payload: {
}, rawFileDetails: {
importStatus: { name: "",
isImported: true,
tagged: false,
matchedResult: {
score: "0",
}, },
}, importStatus: {
sourcedMetadata: payload || null, isImported: true,
acquisition: { source: { wanted: true, name: sourceName } }, tagged: false,
matchedResult: {
score: "0",
},
},
sourcedMetadata: metadata || null,
acquisition: { source: { wanted: true, name: sourceName } },
}
}; };
dispatch({ dispatch({
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS, type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
@@ -194,6 +190,7 @@ export const fetchVolumeGroups = () => async (dispatch) => {
}; };
export const fetchComicVineMatches = export const fetchComicVineMatches =
(searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => { (searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => {
console.log(issueSearchQuery);
try { try {
dispatch({ dispatch({
type: CV_API_CALL_IN_PROGRESS, type: CV_API_CALL_IN_PROGRESS,
@@ -223,7 +220,6 @@ export const fetchComicVineMatches =
}, },
}) })
.then((response) => { .then((response) => {
console.log(response);
let matches: any = []; let matches: any = [];
if ( if (
!isNil(response.data.results) && !isNil(response.data.results) &&
@@ -233,7 +229,6 @@ export const fetchComicVineMatches =
} else { } else {
matches = response.data.map((match) => match); matches = response.data.map((match) => match);
} }
console.log(matches);
dispatch({ dispatch({
type: CV_SEARCH_SUCCESS, type: CV_SEARCH_SUCCESS,
searchResults: matches, searchResults: matches,
@@ -252,33 +247,38 @@ export const fetchComicVineMatches =
}); });
}; };
export const extractComicArchive = (path: string) => async (dispatch) => { /**
const comicBookPages: string[] = []; * This method is a proxy to `uncompressFullArchive` which uncompresses complete `rar` or `zip` archives
dispatch({ * @param {string} path The path to the compressed archive
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS, * @param {any} options Options object
}); * @returns {any}
const extractedComicBookArchive = await axios({ */
method: "POST", export const extractComicArchive =
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,
});
};
(path: string, options: any): any =>
async (dispatch) => {
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
});
await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
filePath: path,
options,
},
});
};
/**
* Description
* @param {any} query
* @param {any} options
* @returns {any}
*/
export const searchIssue = (query, options) => async (dispatch) => { export const searchIssue = (query, options) => async (dispatch) => {
dispatch({ dispatch({
type: SS_SEARCH_IN_PROGRESS, type: SS_SEARCH_IN_PROGRESS,
@@ -317,12 +317,16 @@ export const searchIssue = (query, options) => async (dispatch) => {
data: response.data.body, data: response.data.body,
}); });
break; break;
case "volumesPage":
dispatch({
type: VOLUMES_FETCHED,
data: response.data.body,
});
break;
default: default:
break; break;
} }
}; };
export const analyzeImage = export const analyzeImage =
(imageFilePath: string | Buffer) => async (dispatch) => { (imageFilePath: string | Buffer) => async (dispatch) => {
@@ -345,4 +349,4 @@ export const analyzeImage =
type: IMG_ANALYSIS_DATA_FETCH_SUCCESS, type: IMG_ANALYSIS_DATA_FETCH_SUCCESS,
result: foo.data, result: foo.data,
}); });
}; };

View File

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

View File

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

View File

@@ -4,11 +4,25 @@ import { useDispatch } from "react-redux";
import { saveSettings, deleteSettings } from "../../actions/settings.actions"; import { saveSettings, deleteSettings } from "../../actions/settings.actions";
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation"; import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import { isUndefined, isEmpty } from "lodash"; import { isUndefined, isEmpty, isNil } from "lodash";
export const AirDCPPSettingsForm = (): ReactElement => { export const AirDCPPSettingsForm = (): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const airDCPPSettings = useContext(AirDCPPSocketContext); 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) => { const onSubmit = useCallback(async (values) => {
try { try {
airDCPPSettings.setSettings(values); airDCPPSettings.setSettings(values);
@@ -25,7 +39,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,
) )
@@ -52,12 +66,24 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</Field> </Field>
</span> </span>
</p> </p>
<p className="control is-expanded"> <div className="control is-expanded">
<Field <Field
name="hostname" name="hostname"
validate={hostValidator}>
{({ input, meta }) => (
<div>
<input {...input} type="text" placeholder="AirDC++ hostname" className="input" />
{meta.error && meta.touched && <span className="is-size-7 has-text-danger">{meta.error}</span>}
</div>
)}
</Field>
</div>
<p className="control">
<Field
name="port"
component="input" component="input"
className="input" className="input"
placeholder="AirDC++ host IP / hostname" placeholder="AirDC++ port"
/> />
</p> </p>
</div> </div>

View File

@@ -3,14 +3,11 @@ import Dashboard from "./Dashboard/Dashboard";
import Import from "./Import"; import Import from "./Import";
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer"; import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
import LibraryContainer from "./Library/LibraryContainer"; import TabulatedContentContainer from "./Library/TabulatedContentContainer";
import LibraryGrid from "./Library/LibraryGrid"; import LibraryGrid from "./Library/LibraryGrid";
import Search from "./Search"; import Search from "./Search";
import Settings from "./Settings"; import Settings from "./Settings";
import VolumeDetail from "./VolumeDetail/VolumeDetail"; 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 Downloads from "./Downloads/Downloads";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
@@ -21,17 +18,24 @@ import {
AirDCPPSocketContext, AirDCPPSocketContext,
} from "../context/AirDCPPSocket"; } from "../context/AirDCPPSocket";
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types"; import {
import { useDispatch } from "react-redux"; AIRDCPP_DOWNLOAD_PROGRESS_TICK,
import axios from "axios"; LS_SINGLE_IMPORT,
import { LIBRARY_SERVICE_BASE_URI } from "../constants/endpoints"; } from "../constants/action-types";
import { useParams } from "react-router"; 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 AirDCPPSocketComponent = (): ReactElement => {
const airDCPPConfiguration = useContext(AirDCPPSocketContext); const airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
const foo = async () => { const initializeAirDCPPEventListeners = async () => {
if ( if (
!isUndefined(airDCPPConfiguration.airDCPPState) && !isUndefined(airDCPPConfiguration.airDCPPState) &&
!isEmpty(airDCPPConfiguration.airDCPPState.settings) && !isEmpty(airDCPPConfiguration.airDCPPState.settings) &&
@@ -42,9 +46,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
"queue_bundle_added", "queue_bundle_added",
async (data) => { async (data) => {
console.log("JEMEN:", data); console.log("JEMEN:", data);
},
}
); );
// download tick listener // download tick listener
await airDCPPConfiguration.airDCPPState.socket.addListener( await airDCPPConfiguration.airDCPPState.socket.addListener(
@@ -62,9 +64,18 @@ const AirDCPPSocketComponent = (): ReactElement => {
`queue`, `queue`,
"queue_bundle_status", "queue_bundle_status",
async (bundleData) => { async (bundleData) => {
let count = 0;
if (bundleData.status.completed && bundleData.status.downloaded) { if (bundleData.status.completed && bundleData.status.downloaded) {
// dispatch the action for raw import, with the metadata // dispatch the action for raw import, with the metadata
console.log("IM THE MAN UP IN THIS") if (count < 1) {
console.log(`[AirDCPP]: Download complete.`);
dispatch({
type: LS_SINGLE_IMPORT,
meta: { remote: true },
data: bundleData,
});
count += 1;
}
} }
}, },
); );
@@ -79,7 +90,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
); );
} }
}; };
foo(); initializeAirDCPPEventListeners();
}, [airDCPPConfiguration]); }, [airDCPPConfiguration]);
return <></>; return <></>;
}; };
@@ -92,7 +103,10 @@ export const App = (): ReactElement => {
<Routes> <Routes>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} /> <Route path="/import" element={<Import path={"./comics"} />} />
<Route path="/library" element={<LibraryContainer />} /> <Route
path="/library"
element={<TabulatedContentContainer category="library" />}
/>
<Route path="/library-grid" element={<LibraryGrid />} /> <Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} /> <Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
@@ -105,9 +119,18 @@ export const App = (): ReactElement => {
element={<VolumeDetail />} element={<VolumeDetail />}
/> />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/pull-list/all" element={<PullList />} /> <Route
<Route path="/wanted/all" element={<WantedComics />} /> path="/pull-list/all"
<Route path="/volumes/all" element={<Volumes />} /> element={<TabulatedContentContainer category="pullList" />}
/>
<Route
path="/wanted/all"
element={<TabulatedContentContainer category="wanted" />}
/>
<Route
path="/volumes/all"
element={<TabulatedContentContainer category="volumes" />}
/>
</Routes> </Routes>
</div> </div>
</AirDCPPSocketContextProvider> </AirDCPPSocketContextProvider>

View File

@@ -16,6 +16,7 @@ import ellipsize from "ellipsize";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { isEmpty, isNil, map } from "lodash"; import { isEmpty, isNil, map } from "lodash";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
interface IAcquisitionPanelProps { interface IAcquisitionPanelProps {
query: any; query: any;
comicObjectId: any; comicObjectId: any;
@@ -96,9 +97,12 @@ export const AcquisitionPanel = (
(searchInstanceId, resultId, name, size, type) => { (searchInstanceId, resultId, name, size, type) => {
dispatch( dispatch(
downloadAirDCPPItem( downloadAirDCPPItem(
searchInstanceId, resultId, searchInstanceId,
resultId,
props.comicObjectId, props.comicObjectId,
name, size, type, name,
size,
type,
airDCPPConfiguration.airDCPPState.socket, airDCPPConfiguration.airDCPPState.socket,
{ {
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,
@@ -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)) { } else if (!isEmpty(data.sourcedMetadata)) {
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name); issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
} }
dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery)); dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery));
setSlidingPanelContentId("CVMatches"); setSlidingPanelContentId("CVMatches");
setVisible(true); setVisible(true);

View File

@@ -5,6 +5,7 @@ import Card from "../Carda";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { RawFileDetails } from "./RawFileDetails"; import { RawFileDetails } from "./RawFileDetails";
import { ComicVineSearchForm } from "../ComicVineSearchForm";
import TabControls from "./TabControls"; import TabControls from "./TabControls";
import { EditMetadataPanel } from "./EditMetadataPanel"; import { EditMetadataPanel } from "./EditMetadataPanel";
@@ -40,6 +41,15 @@ type ComicDetailProps = {};
*/ */
export const ComicDetail = (data: ComicDetailProps): ReactElement => { export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
},
userSettings,
} = data;
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState(""); const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
@@ -56,7 +66,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
); );
const extractedComicBook = useSelector( const extractedComicBook = useSelector(
(state: RootState) => state.fileOps.extractedComicBookArchive, (state: RootState) => state.fileOps.extractedComicBookArchive.reading,
); );
const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicObjectId } = useParams<{ comicObjectId: string }>();
@@ -64,12 +74,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const openModal = useCallback((filePath) => { const openModal = useCallback((filePath) => {
setIsOpen(true); 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. // references are now sync'd and can be accessed.
// subtitle.style.color = "#f00"; // subtitle.style.color = "#f00";
console.log("kolaveri", things);
}, []); }, []);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
@@ -79,9 +98,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
// sliding panel init // sliding panel init
const contentForSlidingPanel = { const contentForSlidingPanel = {
CVMatches: { CVMatches: {
content: () => { content: (props) => (
if (!comicVineAPICallProgress) { <>
return ( <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 <ComicVineMatchPanel
props={{ props={{
comicVineSearchQueryObject, comicVineSearchQueryObject,
@@ -90,9 +121,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
comicObjectId, comicObjectId,
}} }}
/> />
); ) : (
} else {
return (
<div className="progress-indicator-container"> <div className="progress-indicator-container">
<div className="indicator"> <div className="indicator">
<Loader <Loader
@@ -105,23 +134,15 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
/> />
</div> </div>
</div> </div>
); )}
} </>
}, ),
}, },
editComicBookMetadata: { editComicBookMetadata: {
content: () => <EditMetadataPanel />, content: () => <EditMetadataPanel />,
}, },
}; };
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
},
userSettings,
} = data;
// check for the availability of CV metadata // check for the availability of CV metadata
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =
@@ -238,7 +259,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
</div> </div>
</div> </div>
{/* raw file details */} {/* raw file details */}
<div className="column is-three-fifths"> <div className="column">
{!isUndefined(rawFileDetails) && {!isUndefined(rawFileDetails) &&
!isEmpty(rawFileDetails.cover) && ( !isEmpty(rawFileDetails.cover) && (
<> <>
@@ -269,6 +290,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<ComicViewer <ComicViewer
pages={extractedComicBook} pages={extractedComicBook}
direction="ltr" direction="ltr"
className={{closeButton: "border: 1px solid red;"}}
/> />
)} )}
</Modal> </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 => { export const ComicVineDetails = (props): ReactElement => {
const { data, updatedAt } = props; const { data, updatedAt } = props;
return ( return (
<div className="column is-two-thirds"> <div className="column is-half">
<div className="comic-detail comicvine-metadata"> <div className="comic-detail comicvine-metadata">
<dl> <dl>
<dt>ComicVine Metadata</dt> <dt>ComicVine Metadata</dt>

View File

@@ -10,38 +10,8 @@ export const ComicVineMatchPanel = (comicVineData): ReactElement => {
comicVineAPICallProgress, comicVineAPICallProgress,
comicVineSearchResults, comicVineSearchResults,
} = comicVineData.props; } = comicVineData.props;
console.log(comicVineData);
return ( 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"> <div className="search-results-container">
{!isEmpty(comicVineSearchResults) && ( {!isEmpty(comicVineSearchResults) && (
<MatchResult <MatchResult

View File

@@ -7,7 +7,7 @@ export const RawFileDetails = (props): ReactElement => {
const { rawFileDetails, inferredMetadata } = props.data; const { rawFileDetails, inferredMetadata } = props.data;
return ( return (
<> <>
<div className="comic-detail raw-file-details"> <div className="comic-detail raw-file-details column is-three-fifths">
<dl> <dl>
<dt>Raw File Details</dt> <dt>Raw File Details</dt>
<dd className="is-size-7"> <dd className="is-size-7">
@@ -38,7 +38,8 @@ export const RawFileDetails = (props): ReactElement => {
</dd> </dd>
</dl> </dl>
</div> </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> <dl>
{/* inferred metadata */} {/* inferred metadata */}
<dt>Inferred Issue Metadata</dt> <dt>Inferred Issue Metadata</dt>
@@ -93,4 +94,4 @@ RawFileDetails.propTypes = {
}), }),
}), }),
}), }),
}; };

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2022-8-9", startDate: "2022-12-25",
pageSize: "15", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),
@@ -127,7 +127,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 +161,4 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
); );
}; };
export default PullList; export default PullList;

View File

@@ -2,7 +2,6 @@ import React, { ReactElement } from "react";
import Card from "../Carda"; import Card from "../Carda";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { escapePoundSymbol } from "../../shared/utils/formatting.utils";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map } from "lodash";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
@@ -24,7 +23,6 @@ export const RecentlyImported = ({
700: 2, 700: 2,
600: 2, 600: 2,
}; };
return ( return (
<> <>
<div className="content mt-5"> <div className="content mt-5">
@@ -41,7 +39,7 @@ export const RecentlyImported = ({
columnClassName="recent-comics-column" columnClassName="recent-comics-column"
> >
{map( {map(
comicBookCovers.docs, comicBookCovers,
( (
{ {
_id, _id,
@@ -64,7 +62,6 @@ export const RecentlyImported = ({
comicInfo, comicInfo,
locg, locg,
}); });
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine) &&
!isUndefined(comicvine.volumeInformation); !isUndefined(comicvine.volumeInformation);
@@ -123,7 +120,7 @@ export const RecentlyImported = ({
</div> </div>
</Card> </Card>
{/* metadata card */} {/* metadata card */}
{!isNil(name) ? ( {!isNil(name) && (
<Card orientation="horizontal" hasDetails imageUrl={coverURL}> <Card orientation="horizontal" hasDetails imageUrl={coverURL}>
<dd className="is-size-9"> <dd className="is-size-9">
<dl> <dl>
@@ -138,7 +135,7 @@ export const RecentlyImported = ({
</dl> </dl>
</dd> </dd>
</Card> </Card>
) : null} )}
</React.Fragment> </React.Fragment>
); );
}, },
@@ -146,4 +143,4 @@ export const RecentlyImported = ({
</Masonry> </Masonry>
</> </>
); );
}; };

View File

@@ -1,22 +1,46 @@
import React, { useMemo, ReactElement, useCallback } from "react"; import React, { useMemo, ReactElement, useCallback, useEffect } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import T2Table from "../shared/T2Table";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
import MetadataPanel from "../shared/MetadataPanel"; import MetadataPanel from "../shared/MetadataPanel";
import SearchBar from "./SearchBar"; import T2Table from "../shared/T2Table";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
interface IComicBookLibraryProps { /**
data: { * Component that tabulates the contents of the user's ThreeTwo Library.
searchResults: any; *
}; * @component
} * @example
* <Library />
export const Library = (data: IComicBookLibraryProps): ReactElement => { */
const { searchResults } = data.data; export const Library = (): ReactElement => {
const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics,
);
const searchError = useSelector((state: RootState) => {
console.log(state);
return state.fileOps.librarySearchError;
});
const dispatch = useDispatch();
useEffect(() => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: 15,
from: 0,
},
type: "all",
trigger: "libraryPage",
},
),
);
}, []);
// programatically navigate to comic detail // programatically navigate to comic detail
const navigate = useNavigate(); const navigate = useNavigate();
@@ -65,7 +89,8 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
const WantedStatus = ({ value }) => { const WantedStatus = ({ value }) => {
return !value ? <span className="tag is-info is-light">Wanted</span> : null; return !value ? <span className="tag is-info is-light">Wanted</span> : null;
}; };
const columns = [ const columns = useMemo(
() => [
{ {
header: "Comic Metadata", header: "Comic Metadata",
footer: 1, footer: 1,
@@ -75,7 +100,7 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
id: "fileDetails", id: "fileDetails",
minWidth: 400, minWidth: 400,
accessorKey: "_source", accessorKey: "_source",
cell: info => { cell: (info) => {
return <MetadataPanel data={info.getValue()} />; return <MetadataPanel data={info.getValue()} />;
}, },
}, },
@@ -84,11 +109,11 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
accessorKey: "_source.sourcedMetadata.comicInfo", accessorKey: "_source.sourcedMetadata.comicInfo",
align: "center", align: "center",
minWidth: 250, minWidth: 250,
cell: info => cell: (info) =>
!isEmpty(info.getValue()) ? ( !isEmpty(info.getValue()) ? (
<ComicInfoXML data={info.getValue()} /> <ComicInfoXML data={info.getValue()} />
) : ( ) : (
<span className="tag">No ComicInfo.xml</span> <span className="tag mt-5">No ComicInfo.xml</span>
), ),
}, },
], ],
@@ -98,13 +123,12 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
columns: [ columns: [
{ {
header: "Publisher", header: "Publisher",
accessorKey: accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation",
"_source.sourcedMetadata.comicvine.volumeInformation", cell: (info) => {
cell: info => {
return ( return (
!isNil(info.getValue()) && ( !isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold"> <h6 className="is-size-7 has-text-weight-bold">
{ info.getValue().publisher.name } {info.getValue().publisher.name}
</h6> </h6>
) )
); );
@@ -113,21 +137,29 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
{ {
header: "Something", header: "Something",
accessorKey: "_source.acquisition.source.wanted", accessorKey: "_source.acquisition.source.wanted",
cell: info => { cell: (info) => {
!isUndefined(info.getValue()) ? !isUndefined(info.getValue()) ? (
<WantedStatus value={info.getValue().toString()} /> : "Nothing"; <WantedStatus value={info.getValue().toString()} />
) : (
"Nothing"
);
}, },
}, },
], ],
} },
] ],
[],
);
// ImportStatus.propTypes = { /**
// value: PropTypes.bool.isRequired, * Pagination control that fetches the next x (pageSize) items
// }; * based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
* @param {number} pageIndex
const dispatch = useDispatch(); * @param {number} pageSize
const goToNextPage = useCallback((pageIndex, pageSize) => { * @returns void
*
**/
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch( dispatch(
searchIssue( searchIssue(
{ {
@@ -139,17 +171,25 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from: pageSize * pageIndex + 1, from: pageSize * pageIndex + 1,
}, },
type: "all", 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; let from = 0;
if (pageIndex === 2) { if (pageIndex === 2) {
from = (pageIndex - 1) * pageSize + 2 - 27; from = (pageIndex - 1) * pageSize + 2 - 17;
} else { } else {
from = (pageIndex - 1) * pageSize + 2 - 26; from = (pageIndex - 1) * pageSize + 2 - 16;
} }
dispatch( dispatch(
searchIssue( searchIssue(
@@ -162,30 +202,54 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from, from,
}, },
type: "all", type: "all",
trigger: "libraryPage",
}, },
), ),
); );
}, []); }, []);
// ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired,
// };
return ( return (
<section className="container"> <section className="container">
<div className="section"> <div className="section">
<h1 className="title">Library</h1> <div className="header-area">
{/* Search bar */} <h1 className="title">Library</h1>
<SearchBar /> </div>
{!isUndefined(searchResults) && ( {!isEmpty(searchResults) ? (
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table
rowData={searchResults.hits.hits}
totalPages={searchResults.hits.total.value} totalPages={searchResults.hits.total.value}
columns={columns} columns={columns}
paginationHandlers={{ sourceData={searchResults?.hits?.hits}
nextPage: goToNextPage,
previousPage: goToPreviousPage,
}}
rowClickHandler={navigateToComicDetail} 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>
</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

@@ -21,19 +21,20 @@ export const SearchBar = (): ReactElement => {
from: 0, from: 0,
}, },
type: "volumeName", type: "volumeName",
trigger: "libraryPage",
}, },
), ),
); );
}, []); }, []);
return ( return (
<div className="box sticky"> <div className="box">
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
initialValues={{}} initialValues={{}}
render={({ handleSubmit, form, submitting, pristine, values }) => ( render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="field is-grouped"> <div className="field is-grouped">
<div className="control is-expanded search"> <div className="control search is-expanded">
<Field name="search"> <Field name="search">
{({ input, meta }) => { {({ input, meta }) => {
return ( return (
@@ -55,26 +56,7 @@ export const SearchBar = (): ReactElement => {
</form> </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> </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,15 +1,25 @@
import React from "react"; import React, { useContext } from "react";
import { SearchBar } from "./GlobalSearchBar/SearchBar"; import { SearchBar } from "./GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick"; import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { format, fromUnixTime } from "date-fns";
const Navbar: React.FunctionComponent = (props) => { const Navbar: React.FunctionComponent = (props) => {
const downloadProgressTick = useSelector( const downloadProgressTick = useSelector(
(state: RootState) => state.airdcpp.downloadProgressData, (state: RootState) => state.airdcpp.downloadProgressData,
); );
const airDCPPSocketConnectionStatus = useSelector(
(state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
);
const airDCPPSessionInfo = useSelector(
(state: RootState) => state.airdcpp.airDCPPSessionInfo,
);
const socketDisconnectionReason = useSelector(
(state: RootState) => state.airdcpp.socketDisconnectionReason,
);
return ( return (
<nav className="navbar is-fixed-top"> <nav className="navbar is-fixed-top">
<div className="navbar-brand"> <div className="navbar-brand">
@@ -72,18 +82,66 @@ const Navbar: React.FunctionComponent = (props) => {
<div className="navbar-item has-dropdown is-hoverable"> <div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless"> <a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i> <i className="fa-solid fa-download"></i>
{downloadProgressTick && ( {downloadProgressTick && <div className="pulsating-circle"></div>}
<div className="pulsating-circle"></div>
)}
</a> </a>
{!isUndefined(downloadProgressTick) ? ( {!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown download-progress-meter"> <div className="navbar-dropdown is-right">
<a className="navbar-item"> <a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} /> <DownloadProgressTick data={downloadProgressTick} />
</a> </a> </div>
</div>
) : null} ) : null}
</div> </div>
{/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable">
{airDCPPSocketConnectionStatus ? (
<>
<a className="navbar-link is-arrowless has-text-success">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pt-4 pr-2 pl-2 is-right airdcpp-status">
{/* 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">
<pre>
{JSON.stringify(socketDisconnectionReason, null, 2)}
</pre>
</div>
</>
)}
</div>
<div className="navbar-item has-dropdown is-hoverable is-mega"> <div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div> <div className="navbar-link flex">Blog</div>
<div id="blogDropdown" className="navbar-dropdown"> <div id="blogDropdown" className="navbar-dropdown">

View File

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

View File

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

View File

@@ -77,7 +77,9 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
</div> </div>
)} )}
</div> </div>
</div> </div>
</dd> </dd>
</dl> </dl>
), ),
@@ -211,7 +213,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
orientation={"vertical"} orientation={"vertical"}
hasDetails={false} hasDetails={false}
imageStyle={props.imageStyle} imageStyle={props.imageStyle}
// cardContainerStyle={{ maxWidth: 200 }} // cardContainerStyle={{ maxWidth: 200 }}
/> />
</div> </div>
<div className="column">{metadataPanel.content()}</div> <div className="column">{metadataPanel.content()}</div>

View File

@@ -1,27 +1,114 @@
import React, { ReactElement, useState } from "react"; import React, { ReactElement, useMemo, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import SearchBar from "../Library/SearchBar";
import { Link } from "react-router-dom";
import { import {
ColumnDef, ColumnDef,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel,
useReactTable, useReactTable,
PaginationState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
export const T2Table = (tableOptions): ReactElement => { export const T2Table = (tableOptions): ReactElement => {
const { rowData, columns, paginationHandlers, totalPages, rowClickHandler } = const { sourceData, columns, paginationHandlers: { nextPage, previousPage }, totalPages, rowClickHandler } =
tableOptions; tableOptions;
const [isPageSizeDropdownCollapsed, collapsePageSizeDropdown] =
useState(false); const [{ pageIndex, pageSize }, setPagination] =
const togglePageSizeDropdown = () => useState<PaginationState>({
collapsePageSizeDropdown(!isPageSizeDropdownCollapsed); pageIndex: 1,
pageSize: 15,
});
console.log(sourceData)
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({ const table = useReactTable({
data: rowData, data: sourceData,
columns, columns,
manualPagination: true,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
pageCount: sourceData.length ?? -1,
state: {
pagination,
},
onPaginationChange: setPagination,
}); });
return ( 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">
<p className="control">
<div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div>
</p>
<p className="control">
<div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div>
</p>
<div className="field has-addons ml-5">
<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"> <table className="table is-hoverable">
<thead> <thead>
{table.getHeaderGroups().map((headerGroup, idx) => ( {table.getHeaderGroups().map((headerGroup, idx) => (
@@ -60,15 +147,12 @@ export const T2Table = (tableOptions): ReactElement => {
})} })}
</tbody> </tbody>
</table> </table>
{/* pagination control */}
</> </>
); );
}; };
T2Table.propTypes = { T2Table.propTypes = {
rowData: PropTypes.array, sourceData: PropTypes.array,
totalPages: PropTypes.number, totalPages: PropTypes.number,
columns: PropTypes.array, columns: PropTypes.array,
paginationHandlers: PropTypes.shape({ paginationHandlers: PropTypes.shape({

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,12 @@ import {
LS_IMPORT_CALL_IN_PROGRESS, LS_IMPORT_CALL_IN_PROGRESS,
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
VOLUMES_FETCHED,
COMICBOOK_EXTRACTION_SUCCESS,
} from "../constants/action-types"; } from "../constants/action-types";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
const initialState = { const initialState = {
IMSCallInProgress: false, IMSCallInProgress: false,
IMGCallInProgress: false, IMGCallInProgress: false,
@@ -41,10 +46,14 @@ const initialState = {
isComicVineMetadataImportInProgress: false, isComicVineMetadataImportInProgress: false,
comicVineMetadataImportError: {}, comicVineMetadataImportError: {},
rawImportError: {}, rawImportError: {},
extractedComicBookArchive: [], extractedComicBookArchive: {
reading: [],
analysis: [],
},
recentComics: [], recentComics: [],
wantedComics: [], wantedComics: [],
libraryComics: [], libraryComics: [],
volumes: [],
librarySearchResultsFormatted: [], librarySearchResultsFormatted: [],
librarySearchResultCount: 0, librarySearchResultCount: 0,
libraryQueueResults: [], libraryQueueResults: [],
@@ -79,7 +88,7 @@ function fileOpsReducer(state = initialState, action) {
case IMS_RECENT_COMICS_FETCHED: case IMS_RECENT_COMICS_FETCHED:
return { return {
...state, ...state,
recentComics: action.data, recentComics: action.data.docs,
}; };
case IMS_WANTED_COMICS_FETCHED: case IMS_WANTED_COMICS_FETCHED:
return { return {
@@ -129,13 +138,7 @@ function fileOpsReducer(state = initialState, action) {
comicBookExtractionInProgress: true, comicBookExtractionInProgress: true,
}; };
} }
case IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS: {
return {
...state,
extractedComicBookArchive: action.extractedComicBookArchive,
comicBookExtractionInProgress: false,
};
}
case LOCATION_CHANGE: { case LOCATION_CHANGE: {
return { return {
...state, ...state,
@@ -150,11 +153,44 @@ function fileOpsReducer(state = initialState, action) {
} }
case LS_COVER_EXTRACTED: { case LS_COVER_EXTRACTED: {
console.log("BASH", action); console.log("BASH", action);
if(state.recentComics.length === 5) {
state.recentComics.pop();
}
return { return {
...state, ...state,
librarySearchResultCount: state.librarySearchResultCount + 1, librarySearchResultCount: state.librarySearchResultCount + 1,
recentComics: [...state.recentComics, action.result.data.importResult]
}; };
} }
case COMICBOOK_EXTRACTION_SUCCESS: {
const comicBookPages: string[] = [];
map(action.result.files, (page) => {
const pageFilePath = removeLeadingPeriod(page);
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
comicBookPages.push(imagePath);
});
switch (action.result.purpose) {
case "reading":
return {
...state,
extractedComicBookArchive: {
reading: comicBookPages,
},
comicBookExtractionInProgress: false,
};
case "analysis":
return {
...state,
extractedComicBookArchive: {
analysis: comicBookPages,
},
comicBookExtractionInProgress: false,
};
}
}
case LS_QUEUE_DRAINED: { case LS_QUEUE_DRAINED: {
console.log("drained", action); console.log("drained", action);
return { return {
@@ -196,7 +232,6 @@ function fileOpsReducer(state = initialState, action) {
} }
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: { case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
const foo = []; const foo = [];
console.log(action.data.hits)
if (!isUndefined(action.data.hits)) { if (!isUndefined(action.data.hits)) {
map(action.data.hits.hits, ({ _source }) => { map(action.data.hits.hits, ({ _source }) => {
foo.push(_source); foo.push(_source);
@@ -221,7 +256,14 @@ function fileOpsReducer(state = initialState, action) {
SSCallInProgress: false, SSCallInProgress: false,
}; };
} }
case VOLUMES_FETCHED:
return {
...state,
volumes: action.data,
SSCallInProgress: false,
};
case SS_SEARCH_FAILED: { case SS_SEARCH_FAILED: {
return { return {
...state, ...state,
@@ -241,4 +283,4 @@ function fileOpsReducer(state = initialState, action) {
} }
} }
export default fileOpsReducer; export default fileOpsReducer;

View File

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

View File

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

View File

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

1
src/funding.yml Normal file
View File

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

149
yarn.lock
View File

@@ -1283,7 +1283,7 @@
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
"@emotion/babel-plugin@^11.3.0", "@emotion/babel-plugin@^11.7.1": "@emotion/babel-plugin@^11.7.1":
version "11.9.2" version "11.9.2"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95"
integrity sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw== integrity sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw==
@@ -1317,28 +1317,45 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@emotion/is-prop-valid@^1.1.0", "@emotion/is-prop-valid@^1.1.1": "@emotion/hash@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7"
integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==
"@emotion/is-prop-valid@^1.1.0":
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz#f0907a416368cf8df9e410117068e20fe87c0a3a" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz#f0907a416368cf8df9e410117068e20fe87c0a3a"
integrity sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA== integrity sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA==
dependencies: dependencies:
"@emotion/memoize" "^0.7.4" "@emotion/memoize" "^0.7.4"
"@emotion/is-prop-valid@^1.1.2":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83"
integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==
dependencies:
"@emotion/memoize" "^0.8.0"
"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": "@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5":
version "0.7.5" version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
"@emotion/react@11.7.1": "@emotion/memoize@^0.8.0":
version "11.7.1" version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw== integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
"@emotion/react@11.9.0":
version "11.9.0"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.0.tgz#b6d42b1db3bd7511e7a7c4151dc8bc82e14593b8"
integrity sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@emotion/babel-plugin" "^11.7.1"
"@emotion/cache" "^11.7.1" "@emotion/cache" "^11.7.1"
"@emotion/serialize" "^1.0.2" "@emotion/serialize" "^1.0.3"
"@emotion/sheet" "^1.1.0" "@emotion/utils" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5" "@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1" hoist-non-react-statics "^3.3.1"
@@ -1366,21 +1383,32 @@
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.0.0"
csstype "^3.0.2" csstype "^3.0.2"
"@emotion/sheet@^1.1.0", "@emotion/sheet@^1.1.1": "@emotion/serialize@^1.0.3":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0"
integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==
dependencies:
"@emotion/hash" "^0.9.0"
"@emotion/memoize" "^0.8.0"
"@emotion/unitless" "^0.8.0"
"@emotion/utils" "^1.2.0"
csstype "^3.0.2"
"@emotion/sheet@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.1.tgz#015756e2a9a3c7c5f11d8ec22966a8dbfbfac787" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.1.tgz#015756e2a9a3c7c5f11d8ec22966a8dbfbfac787"
integrity sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA== integrity sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA==
"@emotion/styled@11.6.0": "@emotion/styled@11.8.1":
version "11.6.0" version "11.8.1"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d" resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.8.1.tgz#856f6f63aceef0eb783985fa2322e2bf66d04e17"
integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw== integrity sha512-OghEVAYBZMpEquHZwuelXcRjRJQOVayvbmNR0zr174NHdmMgrNkLC6TljKC5h9lZLkN5WGrdUcrKlOJ4phhoTQ==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@emotion/babel-plugin" "^11.3.0" "@emotion/babel-plugin" "^11.7.1"
"@emotion/is-prop-valid" "^1.1.1" "@emotion/is-prop-valid" "^1.1.2"
"@emotion/serialize" "^1.0.2" "@emotion/serialize" "^1.0.2"
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.1.0"
"@emotion/stylis@^0.8.4": "@emotion/stylis@^0.8.4":
version "0.8.5" version "0.8.5"
@@ -1392,11 +1420,21 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@emotion/unitless@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
"@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": "@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf"
integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==
"@emotion/utils@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
"@emotion/weak-memoize@^0.2.5": "@emotion/weak-memoize@^0.2.5":
version "0.2.5" version "0.2.5"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
@@ -4030,9 +4068,9 @@
form-data "^3.0.0" form-data "^3.0.0"
"@types/node@*", "@types/node@>=10.0.0": "@types/node@*", "@types/node@>=10.0.0":
version "18.0.2" version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.2.tgz#a594e580c396c22dd6b1470be81737c79ec0b1b1" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-b947SdS4GH+g2W33wf5FzUu1KLj5FcSIiNWbU1ZyMvt/X7w48ZsVcsQoirIgE/Oq03WT5Qbn/dkY0hePi4ZXcQ== integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0": "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0":
version "16.11.43" version "16.11.43"
@@ -6648,13 +6686,6 @@ character-reference-invalid@^1.0.0:
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
chevrotain@7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-7.0.3.tgz#f9621971a726eba804f7763edace2183d157c5d7"
integrity sha512-G634q7M5EiqNNv+0MKcQES2jmqabbi4PvUDpzjG2t+i1XQFaMCz0o8BZ8lbQbZex4RqkzJ3pOy+UwNLFlQm4eg==
dependencies:
regexp-to-ast "0.5.0"
chokidar@^2.1.8: chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -7082,14 +7113,7 @@ compromise-sentences@^0.3.0:
resolved "https://registry.yarnpkg.com/compromise-sentences/-/compromise-sentences-0.3.0.tgz#61d34039323155bc20dad10f24340daaabf86e26" resolved "https://registry.yarnpkg.com/compromise-sentences/-/compromise-sentences-0.3.0.tgz#61d34039323155bc20dad10f24340daaabf86e26"
integrity sha512-L6d5PXmmyPyWRMoJURYOM0yILhtV37zCOy9ZW8Xd6KgY+2qQBTwuokekaGL9MDpSH3/grRDL+AHKytYpGrTEmA== integrity sha512-L6d5PXmmyPyWRMoJURYOM0yILhtV37zCOy9ZW8Xd6KgY+2qQBTwuokekaGL9MDpSH3/grRDL+AHKytYpGrTEmA==
compromise-strict@^0.0.2: compromise@^13.11.4:
version "0.0.2"
resolved "https://registry.yarnpkg.com/compromise-strict/-/compromise-strict-0.0.2.tgz#e4bf4cc386e8795c6408d3606ebb2cd31cab9fbd"
integrity sha512-1ApkPa6kjmyS1ZFhDRZgttDcnVv+Ci8U0kIIsxRBLCN0vautyPm6FROvBwoLimlU3pgIH3hMIpdNdVfe2yj3Gw==
dependencies:
chevrotain "7.0.3"
compromise@^13.11.3, compromise@^13.11.4:
version "13.11.4" version "13.11.4"
resolved "https://registry.yarnpkg.com/compromise/-/compromise-13.11.4.tgz#3136e6d2c2cca822ba7cebd140c9f27f405b67f1" resolved "https://registry.yarnpkg.com/compromise/-/compromise-13.11.4.tgz#3136e6d2c2cca822ba7cebd140c9f27f405b67f1"
integrity sha512-nBITcNdqIHSVDDluaG6guyFFCSNXN+Hu87fU8VlhkE5Z0PwTZN1nro2O7a8JcUH88nB5EOzrxd9zKfXLSNFqcg== integrity sha512-nBITcNdqIHSVDDluaG6guyFFCSNXN+Hu87fU8VlhkE5Z0PwTZN1nro2O7a8JcUH88nB5EOzrxd9zKfXLSNFqcg==
@@ -7642,9 +7666,9 @@ decimal.js@^10.2.1:
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
decode-uri-component@^0.2.0: decode-uri-component@^0.2.0:
version "0.2.0" version "0.2.2"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
decompress-response@^3.2.0, decompress-response@^3.3.0: decompress-response@^3.2.0, decompress-response@^3.3.0:
version "3.3.0" version "3.3.0"
@@ -8225,9 +8249,9 @@ engine.io-parser@~5.0.3:
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
engine.io@~6.2.0: engine.io@~6.2.0:
version "6.2.0" version "6.2.1"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f"
integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==
dependencies: dependencies:
"@types/cookie" "^0.4.1" "@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12" "@types/cors" "^2.8.12"
@@ -11969,9 +11993,9 @@ json2mq@^0.2.0:
string-convert "^0.2.0" string-convert "^0.2.0"
json5@^1.0.1: json5@^1.0.1:
version "1.0.1" version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies: dependencies:
minimist "^1.2.0" minimist "^1.2.0"
@@ -12935,9 +12959,9 @@ minimist-options@4.1.0:
kind-of "^6.0.3" kind-of "^6.0.3"
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6" version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
minipass-collect@^1.0.2: minipass-collect@^1.0.2:
version "1.0.2" version "1.0.2"
@@ -15281,20 +15305,20 @@ react-collapsible@^2.9.0:
resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.10.0.tgz#57330f9f4f968a41ece49c651b56cf30f9a06d19" resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.10.0.tgz#57330f9f4f968a41ece49c651b56cf30f9a06d19"
integrity sha512-kEVsmlFfXBMTCnU5gwIv19MdmPAhbIPzz5Er37TiJSzRKS0IHrqAKQyQeHEmtoGIQMTcVI46FzE4z3NlVTx77A== integrity sha512-kEVsmlFfXBMTCnU5gwIv19MdmPAhbIPzz5Er37TiJSzRKS0IHrqAKQyQeHEmtoGIQMTcVI46FzE4z3NlVTx77A==
react-comic-viewer@^0.3.5: react-comic-viewer@^0.4.0:
version "0.3.5" version "0.4.0"
resolved "https://registry.yarnpkg.com/react-comic-viewer/-/react-comic-viewer-0.3.5.tgz#ab5f9ba74a4aa2e748c29d11b43e40d2a042d9ba" resolved "https://registry.yarnpkg.com/react-comic-viewer/-/react-comic-viewer-0.4.0.tgz#81e170c4d3924f3312171e3fc7d5d4485466c301"
integrity sha512-xMjvu/JVJ821dYWOuOiZlEEMpgdmUeUtznoRjBlMaFi6WJI52V55/6E+ICg8VqROwiMwmWjp65rGhF1P8hlRLg== integrity sha512-tdse0Vn6cJzEbFUZMlEBcY1LrH+LfCKodJInl5Y5lyIZyKARoejLrQNlJPnarvNcPW0qm6yQkYioaI56pq0U1w==
dependencies: dependencies:
"@emotion/react" "11.7.1" "@emotion/react" "11.9.0"
"@emotion/styled" "11.6.0" "@emotion/styled" "11.8.1"
"@react-hook/window-size" "^3.0.7" "@react-hook/window-size" "^3.0.7"
"@rooks/use-did-update" "^4.10.0" "@rooks/use-did-update" "^4.10.0"
"@rooks/use-outside-click-ref" "4.11.2" "@rooks/use-outside-click-ref" "4.11.2"
nanoid "^3.1.25" nanoid "^3.1.25"
react-full-screen "1.1.0" react-full-screen "1.1.0"
react-icons "4.3.1" react-icons "4.3.1"
react-swipeable "6.2.0" react-swipeable "6.2.2"
screenfull "^5.1.0" screenfull "^5.1.0"
react-day-picker@^8.0.6: react-day-picker@^8.0.6:
@@ -15359,7 +15383,7 @@ react-final-form-arrays@^3.1.3:
dependencies: dependencies:
"@babel/runtime" "^7.12.1" "@babel/runtime" "^7.12.1"
react-final-form@^6.5.3: react-final-form@^6.5.9:
version "6.5.9" version "6.5.9"
resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.5.9.tgz#644797d4c122801b37b58a76c87761547411190b" resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.5.9.tgz#644797d4c122801b37b58a76c87761547411190b"
integrity sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA== integrity sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==
@@ -15534,10 +15558,10 @@ react-stickynode@^4.0.0:
shallowequal "^1.0.0" shallowequal "^1.0.0"
subscribe-ui-event "^2.0.6" subscribe-ui-event "^2.0.6"
react-swipeable@6.2.0: react-swipeable@6.2.2:
version "6.2.0" version "6.2.2"
resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-6.2.0.tgz#057271cb7a6fb4af9d2a3f6d80ccdf33e2f64d47" resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-6.2.2.tgz#52ba570f3a7a90db7093094ec476f3d151f727d1"
integrity sha512-nWQ8dEM8e/uswZLSIkXUsAnQmnX4MTcryOHBQIQYRMJFDpgDBSiVbKsz/BZVCIScF4NtJh16oyxwaNOepR6xSw== integrity sha512-Oz7nSFrssvq2yvy05aNL3F+yBUqSvLsK6x1mu+rQFOpMdQVnt4izKt1vyjvvTb70q6GQOaSpaB6qniROW2MAzQ==
react-syntax-highlighter@^15.4.5: react-syntax-highlighter@^15.4.5:
version "15.5.0" version "15.5.0"
@@ -15569,7 +15593,7 @@ react-transition-group@4.4.2, react-transition-group@^4.3.0:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
react@^18.1.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
@@ -15817,11 +15841,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2" extend-shallow "^3.0.2"
safe-regex "^1.1.0" safe-regex "^1.1.0"
regexp-to-ast@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz#56c73856bee5e1fef7f73a00f1473452ab712a24"
integrity sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==
regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3: regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
version "1.4.3" version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"