JsDoc across all project files
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Redux action creators for AirDC++ integration.
|
||||
* Provides actions for Direct Connect protocol operations including
|
||||
* search, download management, and socket connection handling.
|
||||
* @module actions/airdcpp
|
||||
*/
|
||||
|
||||
import {
|
||||
SearchQuery,
|
||||
SearchInstance,
|
||||
@@ -27,16 +34,41 @@ import {
|
||||
import { isNil } from "lodash";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* Configuration data for an AirDC++ search operation.
|
||||
* @interface SearchData
|
||||
* @property {Object} query - Search query parameters
|
||||
* @property {string} query.pattern - Search pattern/term (required)
|
||||
* @property {string[]|undefined|null} hub_urls - List of hub URLs to search
|
||||
* @property {PriorityEnum} priority - Download priority level
|
||||
*/
|
||||
interface SearchData {
|
||||
query: Pick<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
|
||||
hub_urls: string[] | undefined | null;
|
||||
priority: PriorityEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise that resolves after a specified delay.
|
||||
* Useful for rate limiting or adding delays between operations.
|
||||
*
|
||||
* @param {number} ms - Number of milliseconds to sleep
|
||||
* @returns {Promise<NodeJS.Timeout>} Promise that resolves after the delay
|
||||
* @example
|
||||
* await sleep(1000); // Wait 1 second
|
||||
*/
|
||||
export const sleep = (ms: number): Promise<NodeJS.Timeout> => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to toggle AirDC++ socket connection status.
|
||||
* Dispatches connection or disconnection events to update application state.
|
||||
*
|
||||
* @param {String} status - Connection status ("connected" or "disconnected")
|
||||
* @param {any} [payload] - Optional payload data for the status change
|
||||
* @returns {Function} Redux thunk function
|
||||
*/
|
||||
export const toggleAirDCPPSocketConnectionStatus =
|
||||
(status: String, payload?: any) => async (dispatch) => {
|
||||
switch (status) {
|
||||
@@ -58,6 +90,23 @@ export const toggleAirDCPPSocketConnectionStatus =
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to download an item from AirDC++ search results.
|
||||
* Initiates the download, stores bundle metadata in the database, and updates
|
||||
* the comic book record with download information.
|
||||
*
|
||||
* @param {Number} searchInstanceId - ID of the active search instance
|
||||
* @param {String} resultId - ID of the search result to download
|
||||
* @param {String} comicObjectId - ID of the comic book in the database
|
||||
* @param {String} name - Name of the file to download
|
||||
* @param {Number} size - Size of the file in bytes
|
||||
* @param {any} type - File type information
|
||||
* @param {any} ADCPPSocket - AirDC++ socket connection instance
|
||||
* @param {any} credentials - Authentication credentials for AirDC++
|
||||
* @returns {Function} Redux thunk function
|
||||
* @throws {Error} If download initiation or database update fails
|
||||
*/
|
||||
export const downloadAirDCPPItem =
|
||||
(
|
||||
searchInstanceId: Number,
|
||||
@@ -112,6 +161,17 @@ export const downloadAirDCPPItem =
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch AirDC++ download bundles for a specific comic.
|
||||
* Retrieves the comic book record, extracts associated bundle IDs, and fetches
|
||||
* bundle details from the AirDC++ queue.
|
||||
*
|
||||
* @param {string} comicObjectId - ID of the comic book in the database
|
||||
* @param {any} ADCPPSocket - AirDC++ socket connection instance
|
||||
* @param {any} credentials - Authentication credentials for AirDC++
|
||||
* @returns {Function} Redux thunk function that dispatches AIRDCPP_BUNDLES_FETCHED
|
||||
* @throws {Error} If fetching comic or bundles fails
|
||||
*/
|
||||
export const getBundlesForComic =
|
||||
(comicObjectId: string, ADCPPSocket: any, credentials: any) =>
|
||||
async (dispatch) => {
|
||||
@@ -147,6 +207,17 @@ export const getBundlesForComic =
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch all active transfers from AirDC++.
|
||||
* Retrieves current download bundles and groups library issues by their
|
||||
* associated bundle IDs for display in the UI.
|
||||
*
|
||||
* @param {any} ADCPPSocket - AirDC++ socket connection instance
|
||||
* @param {any} credentials - Authentication credentials for AirDC++
|
||||
* @returns {Function} Redux thunk function that dispatches AIRDCPP_TRANSFERS_FETCHED
|
||||
* and LIBRARY_ISSUE_BUNDLES actions
|
||||
* @throws {Error} If fetching transfers fails
|
||||
*/
|
||||
export const getTransfers =
|
||||
(ADCPPSocket: any, credentials: any) => async (dispatch) => {
|
||||
try {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Redux action creators for ComicVine API and comic book information.
|
||||
* Provides actions for searching ComicVine, fetching comic metadata, managing
|
||||
* library statistics, and applying ComicVine matches to local comic records.
|
||||
* @module actions/comicinfo
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import rateLimiter from "axios-rate-limit";
|
||||
import { setupCache } from "axios-cache-interceptor";
|
||||
@@ -22,12 +29,30 @@ import {
|
||||
LIBRARY_SERVICE_BASE_URI,
|
||||
} from "../constants/endpoints";
|
||||
|
||||
/**
|
||||
* Rate-limited axios instance for ComicVine API calls.
|
||||
* Limited to 1 request per second to comply with API rate limits.
|
||||
* @constant {AxiosInstance}
|
||||
*/
|
||||
const http = rateLimiter(axios.create(), {
|
||||
maxRequests: 1,
|
||||
perMilliseconds: 1000,
|
||||
maxRPS: 1,
|
||||
});
|
||||
|
||||
/**
|
||||
* Cached axios instance for reducing redundant API calls.
|
||||
* @constant {AxiosInstance}
|
||||
*/
|
||||
const cachedAxios = setupCache(axios);
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch the weekly comic pull list.
|
||||
* Retrieves upcoming comic releases from the ComicVine service.
|
||||
*
|
||||
* @param {Object} options - Query parameters for the pull list request
|
||||
* @returns {Function} Redux thunk function that dispatches CV_WEEKLY_PULLLIST_FETCHED
|
||||
*/
|
||||
export const getWeeklyPullList = (options) => async (dispatch) => {
|
||||
try {
|
||||
dispatch({
|
||||
@@ -47,6 +72,18 @@ export const getWeeklyPullList = (options) => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic Redux thunk action creator for ComicVine API calls.
|
||||
* Handles rate-limited requests to the ComicVine service with configurable
|
||||
* endpoints, methods, and parameters.
|
||||
*
|
||||
* @param {Object} options - API call configuration options
|
||||
* @param {string} options.callURIAction - API endpoint action (e.g., "search")
|
||||
* @param {string} options.callMethod - HTTP method (GET, POST, etc.)
|
||||
* @param {Object} options.callParams - Query parameters for the request
|
||||
* @param {any} [options.data] - Request body data
|
||||
* @returns {Function} Redux thunk function that dispatches appropriate action based on callURIAction
|
||||
*/
|
||||
export const comicinfoAPICall = (options) => async (dispatch) => {
|
||||
try {
|
||||
dispatch({
|
||||
@@ -82,6 +119,14 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch all issues for a comic series.
|
||||
* Retrieves issue list from ComicVine for a given volume/series.
|
||||
*
|
||||
* @param {string} comicObjectID - ComicVine volume/series ID
|
||||
* @returns {Function} Redux thunk function that dispatches CV_ISSUES_FOR_VOLUME_IN_LIBRARY_SUCCESS
|
||||
*/
|
||||
export const getIssuesForSeries =
|
||||
(comicObjectID: string) => async (dispatch) => {
|
||||
dispatch({
|
||||
@@ -104,6 +149,18 @@ export const getIssuesForSeries =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to analyze library issues against ComicVine data.
|
||||
* Maps issues to query objects and finds matching issues in the local library.
|
||||
*
|
||||
* @param {Array} issues - Array of ComicVine issue objects to analyze
|
||||
* @param {string} issues[].id - Issue ID
|
||||
* @param {string} issues[].name - Issue name
|
||||
* @param {string} issues[].issue_number - Issue number
|
||||
* @param {Object} issues[].volume - Volume information
|
||||
* @param {string} issues[].volume.name - Volume name
|
||||
* @returns {Function} Redux thunk function that dispatches CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED
|
||||
*/
|
||||
export const analyzeLibrary = (issues) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: CV_ISSUES_METADATA_CALL_IN_PROGRESS,
|
||||
@@ -131,6 +188,12 @@ export const analyzeLibrary = (issues) => async (dispatch) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch library statistics.
|
||||
* Retrieves aggregate statistics about the comic library.
|
||||
*
|
||||
* @returns {Function} Redux thunk function that dispatches LIBRARY_STATISTICS_FETCHED
|
||||
*/
|
||||
export const getLibraryStatistics = () => async (dispatch) => {
|
||||
dispatch({
|
||||
type: LIBRARY_STATISTICS_CALL_IN_PROGRESS,
|
||||
@@ -146,6 +209,13 @@ export const getLibraryStatistics = () => async (dispatch) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch detailed comic book information.
|
||||
* Retrieves full comic book document from the library database by ID.
|
||||
*
|
||||
* @param {string} comicBookObjectId - Database ID of the comic book
|
||||
* @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOK_DB_OBJECT_FETCHED
|
||||
*/
|
||||
export const getComicBookDetailById =
|
||||
(comicBookObjectId: string) => async (dispatch) => {
|
||||
dispatch({
|
||||
@@ -166,6 +236,13 @@ export const getComicBookDetailById =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch multiple comic books by their IDs.
|
||||
* Retrieves full comic book documents from the library database for a list of IDs.
|
||||
*
|
||||
* @param {Array<string>} comicBookObjectIds - Array of database IDs
|
||||
* @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOKS_DB_OBJECTS_FETCHED
|
||||
*/
|
||||
export const getComicBooksDetailsByIds =
|
||||
(comicBookObjectIds: Array<string>) => async (dispatch) => {
|
||||
dispatch({
|
||||
@@ -185,6 +262,15 @@ export const getComicBooksDetailsByIds =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to apply ComicVine metadata to a local comic.
|
||||
* Associates a ComicVine match with a comic book record in the database,
|
||||
* updating the comic with metadata from ComicVine.
|
||||
*
|
||||
* @param {Object} match - ComicVine match object containing metadata to apply
|
||||
* @param {string} comicObjectId - Database ID of the local comic book to update
|
||||
* @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOK_DB_OBJECT_FETCHED
|
||||
*/
|
||||
export const applyComicVineMatch =
|
||||
(match, comicObjectId) => async (dispatch) => {
|
||||
dispatch({
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Redux action creators for file operations and library management.
|
||||
* Provides actions for importing comics, searching the library, managing folders,
|
||||
* extracting comic archives, and analyzing images.
|
||||
* @module actions/fileops
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { IFolderData } from "threetwo-ui-typings";
|
||||
import {
|
||||
@@ -37,6 +44,13 @@ import {
|
||||
|
||||
import { isNil } from "lodash";
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch library service health status.
|
||||
* Retrieves health information from the library microservice.
|
||||
*
|
||||
* @param {string} [serviceName] - Optional specific service name to check
|
||||
* @returns {Function} Redux thunk function that dispatches LIBRARY_SERVICE_HEALTH
|
||||
*/
|
||||
export const getServiceStatus = (serviceName?: string) => async (dispatch) => {
|
||||
axios
|
||||
.request({
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
/**
|
||||
* @fileoverview Action creators for Metron comic database API integration.
|
||||
* Provides functions to fetch comic metadata resources from the Metron service.
|
||||
* @module actions/metron
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { isNil } from "lodash";
|
||||
import { METRON_SERVICE_URI } from "../constants/endpoints";
|
||||
|
||||
/**
|
||||
* @typedef {Object} MetronResourceResult
|
||||
* @property {Array<{label: string, value: number}>} options - Formatted options for select components
|
||||
* @property {boolean} hasMore - Whether more results are available
|
||||
* @property {Object} additional - Additional pagination data
|
||||
* @property {number|null} additional.page - Next page number, or null if no more pages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches comic resources from the Metron API service.
|
||||
* Returns formatted data suitable for async select/dropdown components.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - Request options for the Metron API
|
||||
* @param {Object} options.query - Query parameters
|
||||
* @param {number} options.query.page - Current page number for pagination
|
||||
* @returns {Promise<MetronResourceResult>} Formatted results with pagination info
|
||||
* @example
|
||||
* const results = await fetchMetronResource({
|
||||
* query: { page: 1, resource: "publishers" }
|
||||
* });
|
||||
* // Returns: { options: [{label: "DC Comics", value: 1}], hasMore: true, additional: { page: 2 } }
|
||||
*/
|
||||
export const fetchMetronResource = async (options) => {
|
||||
const metronResourceResults = await axios.post(
|
||||
`${METRON_SERVICE_URI}/fetchResource`,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Redux action creators for application settings management.
|
||||
* Provides actions for fetching, deleting, and managing settings, as well as
|
||||
* integrations with external services like qBittorrent and Prowlarr.
|
||||
* @module actions/settings
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import {
|
||||
SETTINGS_OBJECT_FETCHED,
|
||||
@@ -11,6 +18,13 @@ import {
|
||||
QBITTORRENT_SERVICE_BASE_URI,
|
||||
} from "../constants/endpoints";
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to fetch application settings.
|
||||
* Can retrieve all settings or a specific settings key.
|
||||
*
|
||||
* @param {string} [settingsKey] - Optional specific settings key to fetch
|
||||
* @returns {Function} Redux thunk function that dispatches SETTINGS_OBJECT_FETCHED
|
||||
*/
|
||||
export const getSettings = (settingsKey?) => async (dispatch) => {
|
||||
const result = await axios({
|
||||
url: `${SETTINGS_SERVICE_BASE_URI}/getSettings`,
|
||||
@@ -25,6 +39,12 @@ export const getSettings = (settingsKey?) => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to delete all application settings.
|
||||
* Clears the settings from the database and resets state to empty object.
|
||||
*
|
||||
* @returns {Function} Redux thunk function that dispatches SETTINGS_OBJECT_FETCHED with empty data
|
||||
*/
|
||||
export const deleteSettings = () => async (dispatch) => {
|
||||
const result = await axios({
|
||||
url: `${SETTINGS_SERVICE_BASE_URI}/deleteSettings`,
|
||||
@@ -39,6 +59,12 @@ export const deleteSettings = () => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to flush the entire database.
|
||||
* WARNING: This action is destructive and removes all data from the library database.
|
||||
*
|
||||
* @returns {Function} Redux thunk function that dispatches SETTINGS_DB_FLUSH_SUCCESS
|
||||
*/
|
||||
export const flushDb = () => async (dispatch) => {
|
||||
dispatch({
|
||||
type: SETTINGS_CALL_IN_PROGRESS,
|
||||
@@ -57,6 +83,17 @@ export const flushDb = () => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to connect and fetch qBittorrent client information.
|
||||
* Establishes connection to qBittorrent client and retrieves torrent list.
|
||||
*
|
||||
* @param {Object} hostInfo - Connection details for qBittorrent
|
||||
* @param {string} hostInfo.host - qBittorrent server hostname
|
||||
* @param {number} hostInfo.port - qBittorrent server port
|
||||
* @param {string} [hostInfo.username] - Authentication username
|
||||
* @param {string} [hostInfo.password] - Authentication password
|
||||
* @returns {Function} Redux thunk function that dispatches SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED
|
||||
*/
|
||||
export const getQBitTorrentClientInfo = (hostInfo) => async (dispatch) => {
|
||||
await axios.request({
|
||||
url: `${QBITTORRENT_SERVICE_BASE_URI}/connect`,
|
||||
@@ -74,4 +111,15 @@ export const getQBitTorrentClientInfo = (hostInfo) => async (dispatch) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redux thunk action creator to test Prowlarr connection.
|
||||
* Verifies connection to Prowlarr indexer management service.
|
||||
*
|
||||
* @param {Object} hostInfo - Connection details for Prowlarr
|
||||
* @param {string} hostInfo.host - Prowlarr server hostname
|
||||
* @param {number} hostInfo.port - Prowlarr server port
|
||||
* @param {string} hostInfo.apiKey - Prowlarr API key
|
||||
* @returns {Function} Redux thunk function (currently not implemented)
|
||||
* @todo Implement Prowlarr connection verification
|
||||
*/
|
||||
export const getProwlarrConnectionInfo = (hostInfo) => async (dispatch) => {};
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Root application component.
|
||||
* Provides the main layout structure with navigation, content outlet,
|
||||
* and toast notifications. Initializes socket connection on mount.
|
||||
* @module components/App
|
||||
*/
|
||||
|
||||
import React, { ReactElement, useEffect } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Navbar2 } from "./shared/Navbar2";
|
||||
@@ -5,6 +12,26 @@ import { ToastContainer } from "react-toastify";
|
||||
import "../../app.css";
|
||||
import { useStore } from "../store";
|
||||
|
||||
/**
|
||||
* Root application component that provides the main layout structure.
|
||||
*
|
||||
* Features:
|
||||
* - Initializes WebSocket connection to the server on mount
|
||||
* - Renders the navigation bar across all routes
|
||||
* - Provides React Router outlet for child routes
|
||||
* - Includes toast notification container for app-wide notifications
|
||||
*
|
||||
* @returns {ReactElement} The root application layout
|
||||
* @example
|
||||
* // Used as the root element in React Router configuration
|
||||
* const router = createBrowserRouter([
|
||||
* {
|
||||
* path: "/",
|
||||
* element: <App />,
|
||||
* children: [...]
|
||||
* }
|
||||
* ]);
|
||||
*/
|
||||
export const App = (): ReactElement => {
|
||||
useEffect(() => {
|
||||
useStore.getState().getSocket("/"); // Connect to the base namespace
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Import page component for managing comic library imports.
|
||||
* Provides UI for starting imports, monitoring progress, viewing history,
|
||||
* and handling directory configuration issues.
|
||||
* @module components/Import/Import
|
||||
*/
|
||||
|
||||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { isEmpty } from "lodash";
|
||||
@@ -10,16 +17,40 @@ import { RealTimeImportStats } from "./RealTimeImportStats";
|
||||
import { useImportSessionStatus } from "../../hooks/useImportSessionStatus";
|
||||
import { SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||
|
||||
/**
|
||||
* Represents an issue with a configured directory.
|
||||
* @interface DirectoryIssue
|
||||
* @property {string} directory - Path to the directory with issues
|
||||
* @property {string} issue - Description of the issue
|
||||
*/
|
||||
interface DirectoryIssue {
|
||||
directory: string;
|
||||
issue: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of directory status check from the backend.
|
||||
* @interface DirectoryStatus
|
||||
* @property {boolean} isValid - Whether all required directories are accessible
|
||||
* @property {DirectoryIssue[]} issues - List of specific issues found
|
||||
*/
|
||||
interface DirectoryStatus {
|
||||
isValid: boolean;
|
||||
issues: DirectoryIssue[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Import page component for managing comic library imports.
|
||||
*
|
||||
* Features:
|
||||
* - Real-time import progress tracking via WebSocket
|
||||
* - Directory status validation before import
|
||||
* - Force re-import functionality for fixing indexing issues
|
||||
* - Past import history table
|
||||
* - Session management for import tracking
|
||||
*
|
||||
* @returns {ReactElement} The import page UI
|
||||
*/
|
||||
export const Import = (): ReactElement => {
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Real-time import statistics component with live progress tracking.
|
||||
* Displays import statistics, progress bars, and file detection notifications
|
||||
* using WebSocket events for real-time updates.
|
||||
* @module components/Import/RealTimeImportStats
|
||||
*/
|
||||
|
||||
import { ReactElement, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
@@ -15,7 +22,7 @@ import { StatsCard } from "../shared/StatsCard";
|
||||
import { ProgressBar } from "../shared/ProgressBar";
|
||||
|
||||
/**
|
||||
* RealTimeImportStats component displays import statistics with a card-based layout and progress bar.
|
||||
* Real-time import statistics component with card-based layout and progress tracking.
|
||||
*
|
||||
* This component manages three distinct states:
|
||||
* - **Pre-import (idle)**: Shows current file counts and "Start Import" button when new files exist
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
/**
|
||||
* @fileoverview Reusable alert card component for displaying status messages.
|
||||
* Supports multiple variants (error, warning, info, success) with consistent
|
||||
* styling and optional dismiss functionality.
|
||||
* @module components/shared/AlertCard
|
||||
*/
|
||||
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Visual style variants for the alert card.
|
||||
* @typedef {"error"|"warning"|"info"|"success"} AlertVariant
|
||||
*/
|
||||
export type AlertVariant = "error" | "warning" | "info" | "success";
|
||||
|
||||
interface AlertCardProps {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/**
|
||||
* @fileoverview Reusable progress bar component with percentage display.
|
||||
* Supports animated shimmer effect for active states and customizable labels.
|
||||
* @module components/shared/ProgressBar
|
||||
*/
|
||||
|
||||
import { ReactElement } from "react";
|
||||
|
||||
/**
|
||||
* Props for the ProgressBar component.
|
||||
* @interface ProgressBarProps
|
||||
*/
|
||||
interface ProgressBarProps {
|
||||
/** Current progress value */
|
||||
current: number;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/**
|
||||
* @fileoverview Reusable stats card component for displaying numeric metrics.
|
||||
* Used for dashboards and import statistics displays.
|
||||
* @module components/shared/StatsCard
|
||||
*/
|
||||
|
||||
import { ReactElement } from "react";
|
||||
|
||||
/**
|
||||
* Props for the StatsCard component.
|
||||
* @interface StatsCardProps
|
||||
*/
|
||||
interface StatsCardProps {
|
||||
/** The main numeric value to display */
|
||||
value: number;
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
/**
|
||||
* @fileoverview Redux action type constants for the application.
|
||||
* Organizes action types by feature/domain for better maintainability.
|
||||
* @module constants/action-types
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// COMICVINE API ACTION TYPES
|
||||
// =============================================================================
|
||||
|
||||
/** @constant {string} CV_API_CALL_IN_PROGRESS - ComicVine API call started */
|
||||
export const CV_API_CALL_IN_PROGRESS = "CV_SEARCH_IN_PROGRESS";
|
||||
/** @constant {string} CV_SEARCH_FAILURE - ComicVine search failed */
|
||||
export const CV_SEARCH_FAILURE = "CV_SEARCH_FAILURE";
|
||||
/** @constant {string} CV_SEARCH_SUCCESS - ComicVine search completed successfully */
|
||||
export const CV_SEARCH_SUCCESS = "CV_SEARCH_SUCCESS";
|
||||
/** @constant {string} CV_CLEANUP - Reset ComicVine state */
|
||||
export const CV_CLEANUP = "CV_CLEANUP";
|
||||
|
||||
/** @constant {string} CV_API_GENERIC_FAILURE - Generic ComicVine API error */
|
||||
export const CV_API_GENERIC_FAILURE = "CV_API_GENERIC_FAILURE";
|
||||
|
||||
export const IMS_COMICBOOK_METADATA_FETCHED = "IMS_SOCKET_DATA_FETCHED";
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/**
|
||||
* @fileoverview API endpoint configuration constants.
|
||||
* Builds URIs for all microservices used by the application.
|
||||
* Supports environment-based configuration via Vite environment variables.
|
||||
* @module constants/endpoints
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constructs a full URI from protocol, host, port, and path components.
|
||||
*
|
||||
* @param {Record<string, string>} options - URI component options
|
||||
* @param {string} options.protocol - Protocol (http, https, ws, wss)
|
||||
* @param {string} options.host - Hostname or IP address
|
||||
* @param {string} options.port - Port number
|
||||
* @param {string} options.apiPath - API path prefix
|
||||
* @returns {string} Complete URI string
|
||||
* @example
|
||||
* hostURIBuilder({ protocol: "http", host: "localhost", port: "3000", apiPath: "/api" })
|
||||
* // Returns "http://localhost:3000/api"
|
||||
*/
|
||||
export const hostURIBuilder = (options: Record<string, string>): string => {
|
||||
return (
|
||||
options.protocol +
|
||||
@@ -9,6 +29,14 @@ export const hostURIBuilder = (options: Record<string, string>): string => {
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// SERVICE ENDPOINT CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* CORS proxy server URI for bypassing cross-origin restrictions.
|
||||
* @constant {string}
|
||||
*/
|
||||
export const CORS_PROXY_SERVER_URI = hostURIBuilder({
|
||||
protocol: "http",
|
||||
host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost",
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
/**
|
||||
* @fileoverview Adapter functions for transforming GraphQL responses to legacy formats.
|
||||
* Enables gradual migration from REST API to GraphQL while maintaining backward
|
||||
* compatibility with existing components and data structures.
|
||||
* @module graphql/adapters/comicAdapter
|
||||
*/
|
||||
|
||||
import { GetComicByIdQuery } from '../generated';
|
||||
|
||||
/**
|
||||
* Adapter to transform GraphQL Comic response to legacy REST API format
|
||||
* This allows gradual migration while maintaining compatibility with existing components
|
||||
* @typedef {Object} LegacyComicFormat
|
||||
* @property {string} _id - Comic document ID
|
||||
* @property {Object} rawFileDetails - Original file information
|
||||
* @property {Object} inferredMetadata - Auto-detected metadata from parsing
|
||||
* @property {Object} sourcedMetadata - Metadata from external sources (ComicVine, LOCG, etc.)
|
||||
* @property {Object} acquisition - Download/acquisition tracking data
|
||||
* @property {string} createdAt - ISO timestamp of creation
|
||||
* @property {string} updatedAt - ISO timestamp of last update
|
||||
* @property {Object} __graphql - Original GraphQL response for forward compatibility
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms a GraphQL Comic query response to the legacy REST API format.
|
||||
* This adapter enables gradual migration by allowing components to work with
|
||||
* both new GraphQL data and legacy data structures.
|
||||
*
|
||||
* Handles:
|
||||
* - Parsing stringified JSON in sourcedMetadata fields
|
||||
* - Building inferredMetadata from canonical metadata as fallback
|
||||
* - Mapping rawFileDetails to expected structure
|
||||
* - Preserving original GraphQL data for forward compatibility
|
||||
*
|
||||
* @param {GetComicByIdQuery['comic']} graphqlComic - The GraphQL comic response object
|
||||
* @returns {LegacyComicFormat|null} Transformed comic in legacy format, or null if input is null
|
||||
* @example
|
||||
* const { data } = useGetComicByIdQuery({ id: comicId });
|
||||
* const legacyComic = adaptGraphQLComicToLegacy(data?.comic);
|
||||
* // legacyComic now has _id, rawFileDetails, sourcedMetadata, etc.
|
||||
*/
|
||||
export function adaptGraphQLComicToLegacy(graphqlComic: GetComicByIdQuery['comic']) {
|
||||
if (!graphqlComic) return null;
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
/**
|
||||
* @fileoverview GraphQL fetcher utility for React Query integration.
|
||||
* Provides a generic fetcher function that handles GraphQL requests
|
||||
* to the library service backend.
|
||||
* @module graphql/fetcher
|
||||
*/
|
||||
|
||||
import { LIBRARY_SERVICE_HOST } from '../constants/endpoints';
|
||||
|
||||
/**
|
||||
* Creates a GraphQL fetcher function for use with React Query.
|
||||
* Handles POST requests to the GraphQL endpoint with proper error handling.
|
||||
*
|
||||
* @template TData - The expected response data type
|
||||
* @template TVariables - The GraphQL variables type
|
||||
* @param {string} query - The GraphQL query string
|
||||
* @param {TVariables} [variables] - Optional query variables
|
||||
* @param {RequestInit['headers']} [options] - Additional request headers
|
||||
* @returns {Function} Async function that executes the GraphQL request and returns TData
|
||||
* @throws {Error} Throws on HTTP errors or GraphQL errors
|
||||
* @example
|
||||
* const { data } = useQuery({
|
||||
* queryKey: ['comic', id],
|
||||
* queryFn: fetcher<GetComicQuery, GetComicQueryVariables>(
|
||||
* GET_COMIC_QUERY,
|
||||
* { id }
|
||||
* )
|
||||
* });
|
||||
*/
|
||||
export function fetcher<TData, TVariables>(
|
||||
query: string,
|
||||
variables?: TVariables,
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
/**
|
||||
* @fileoverview Custom React hook for managing dark/light theme mode.
|
||||
* Provides theme persistence using localStorage and applies theme classes
|
||||
* to the document root element for CSS-based theming.
|
||||
* @module hooks/useDarkMode
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Custom hook for managing dark mode theme state.
|
||||
* Persists the user's theme preference to localStorage and applies
|
||||
* the appropriate CSS class to the document root element.
|
||||
*
|
||||
* @returns {[string, React.Dispatch<React.SetStateAction<string>>]} A tuple containing:
|
||||
* - theme: The current theme value ("dark" or "light")
|
||||
* - setTheme: State setter function to change the theme
|
||||
* @example
|
||||
* const [theme, setTheme] = useDarkMode();
|
||||
*
|
||||
* // Toggle theme
|
||||
* const toggleTheme = () => {
|
||||
* setTheme(theme === "dark" ? "light" : "dark");
|
||||
* };
|
||||
*
|
||||
* // Use in JSX
|
||||
* <button onClick={toggleTheme}>
|
||||
* Current theme: {theme}
|
||||
* </button>
|
||||
*/
|
||||
export const useDarkMode = () => {
|
||||
const [theme, setTheme] = useState(localStorage.theme);
|
||||
const colorTheme = theme === "dark" ? "light" : "dark";
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
/**
|
||||
* @fileoverview Custom React hook for tracking import session status.
|
||||
* Provides real-time import progress monitoring using Socket.IO events
|
||||
* combined with GraphQL queries for reliable state management.
|
||||
* @module hooks/useImportSessionStatus
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useGetActiveImportSessionQuery } from "../graphql/generated";
|
||||
import { useStore } from "../store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
/**
|
||||
* Possible states for an import session.
|
||||
* @typedef {"idle"|"running"|"completed"|"failed"|"unknown"} ImportSessionStatus
|
||||
*/
|
||||
export type ImportSessionStatus =
|
||||
| "idle" // No import in progress
|
||||
| "running" // Import actively processing
|
||||
@@ -10,6 +21,20 @@ export type ImportSessionStatus =
|
||||
| "failed" // Import finished with errors
|
||||
| "unknown"; // Unable to determine status
|
||||
|
||||
/**
|
||||
* Complete state object representing the current import session.
|
||||
* @interface ImportSessionState
|
||||
* @property {ImportSessionStatus} status - Current status of the import session
|
||||
* @property {string|null} sessionId - Unique identifier for the active session, or null if none
|
||||
* @property {number} progress - Import progress percentage (0-100)
|
||||
* @property {Object|null} stats - Detailed file processing statistics
|
||||
* @property {number} stats.filesQueued - Total files queued for import
|
||||
* @property {number} stats.filesProcessed - Files that have been processed
|
||||
* @property {number} stats.filesSucceeded - Files successfully imported
|
||||
* @property {number} stats.filesFailed - Files that failed to import
|
||||
* @property {boolean} isComplete - Whether the import session has finished
|
||||
* @property {boolean} isActive - Whether import is currently processing
|
||||
*/
|
||||
export interface ImportSessionState {
|
||||
status: ImportSessionStatus;
|
||||
sessionId: string | null;
|
||||
@@ -25,20 +50,32 @@ export interface ImportSessionState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to definitively determine import session completion status
|
||||
* Custom hook to definitively determine import session completion status.
|
||||
*
|
||||
* Uses Socket.IO events to trigger GraphQL refetches:
|
||||
* - IMPORT_SESSION_STARTED: New import started
|
||||
* - IMPORT_SESSION_UPDATED: Progress update
|
||||
* - IMPORT_SESSION_COMPLETED: Import finished
|
||||
* - LS_IMPORT_QUEUE_DRAINED: All jobs processed
|
||||
* Uses Socket.IO events to trigger GraphQL refetches for real-time updates:
|
||||
* - `IMPORT_SESSION_STARTED`: New import started
|
||||
* - `IMPORT_SESSION_UPDATED`: Progress update
|
||||
* - `IMPORT_SESSION_COMPLETED`: Import finished
|
||||
* - `LS_IMPORT_QUEUE_DRAINED`: All jobs processed
|
||||
*
|
||||
* A session is considered DEFINITIVELY COMPLETE when:
|
||||
* - session.status === "completed" OR session.status === "failed"
|
||||
* - OR LS_IMPORT_QUEUE_DRAINED event is received AND no active session exists
|
||||
* - OR IMPORT_SESSION_COMPLETED event is received
|
||||
* - `session.status === "completed"` OR `session.status === "failed"`
|
||||
* - OR `LS_IMPORT_QUEUE_DRAINED` event is received AND no active session exists
|
||||
* - OR `IMPORT_SESSION_COMPLETED` event is received
|
||||
*
|
||||
* NO POLLING - relies entirely on Socket.IO events for real-time updates
|
||||
* @returns {ImportSessionState} Current state of the import session including
|
||||
* status, progress, and file statistics
|
||||
* @example
|
||||
* const { status, progress, stats, isComplete, isActive } = useImportSessionStatus();
|
||||
*
|
||||
* if (isActive) {
|
||||
* console.log(`Import progress: ${progress}%`);
|
||||
* console.log(`Files processed: ${stats?.filesProcessed}/${stats?.filesQueued}`);
|
||||
* }
|
||||
*
|
||||
* if (isComplete && status === "completed") {
|
||||
* console.log("Import finished successfully!");
|
||||
* }
|
||||
*/
|
||||
export const useImportSessionStatus = (): ImportSessionState => {
|
||||
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
/**
|
||||
* @fileoverview Custom React hook for managing real-time import events via Socket.IO.
|
||||
* Provides reactive state tracking for import progress, file detection notifications,
|
||||
* and automatic query invalidation when import status changes.
|
||||
* @module hooks/useImportSocketEvents
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useStore } from "../store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
/**
|
||||
* State object tracking real-time import progress via socket events.
|
||||
* @interface SocketImportState
|
||||
* @property {boolean} active - Whether import is currently in progress
|
||||
* @property {number} completed - Number of successfully completed import jobs
|
||||
* @property {number} total - Total number of jobs in the import queue
|
||||
* @property {number} failed - Number of failed import jobs
|
||||
*/
|
||||
export interface SocketImportState {
|
||||
/** Whether import is currently in progress */
|
||||
active: boolean;
|
||||
@@ -14,6 +29,13 @@ export interface SocketImportState {
|
||||
failed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for the useImportSocketEvents hook.
|
||||
* @interface UseImportSocketEventsReturn
|
||||
* @property {SocketImportState|null} socketImport - Real-time import progress state, null when no import active
|
||||
* @property {string|null} detectedFile - Name of recently detected file for toast notification
|
||||
* @property {Function} clearDetectedFile - Callback to clear the detected file notification
|
||||
*/
|
||||
export interface UseImportSocketEventsReturn {
|
||||
/** Real-time import progress state tracked via socket events */
|
||||
socketImport: SocketImportState | null;
|
||||
@@ -25,13 +47,28 @@ export interface UseImportSocketEventsReturn {
|
||||
|
||||
/**
|
||||
* Custom hook that manages socket event subscriptions for import-related events.
|
||||
*
|
||||
* Subscribes to:
|
||||
* Automatically handles subscription lifecycle and query cache invalidation.
|
||||
*
|
||||
* Subscribes to the following Socket.IO events:
|
||||
* - `LS_LIBRARY_STATS` / `LS_FILES_MISSING`: Triggers statistics refresh
|
||||
* - `LS_FILE_DETECTED`: Shows toast notification for newly detected files
|
||||
* - `LS_INCREMENTAL_IMPORT_STARTED`: Initializes progress tracking
|
||||
* - `LS_COVER_EXTRACTED` / `LS_COVER_EXTRACTION_FAILED`: Updates progress counts
|
||||
* - `LS_IMPORT_QUEUE_DRAINED`: Marks import as complete
|
||||
*
|
||||
* @returns {UseImportSocketEventsReturn} Object containing import state and notification handlers
|
||||
* @example
|
||||
* const { socketImport, detectedFile, clearDetectedFile } = useImportSocketEvents();
|
||||
*
|
||||
* // Show progress bar when import is active
|
||||
* {socketImport?.active && (
|
||||
* <ProgressBar value={socketImport.completed} max={socketImport.total} />
|
||||
* )}
|
||||
*
|
||||
* // Show toast when file detected
|
||||
* {detectedFile && (
|
||||
* <Toast message={`Detected: ${detectedFile}`} onClose={clearDetectedFile} />
|
||||
* )}
|
||||
*/
|
||||
export function useImportSocketEvents(): UseImportSocketEventsReturn {
|
||||
const [socketImport, setSocketImport] = useState<SocketImportState | null>(null);
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
/**
|
||||
* @fileoverview AirDC++ WebSocket service for Direct Connect protocol communication.
|
||||
* Provides a configured socket connection to the AirDC++ daemon for search
|
||||
* and download operations.
|
||||
* @module services/DcppSearchService
|
||||
*/
|
||||
|
||||
import { Socket } from "airdcpp-apisocket";
|
||||
|
||||
/**
|
||||
* Factory class for creating configured AirDC++ WebSocket connections.
|
||||
* Wraps the airdcpp-apisocket library with application-specific configuration.
|
||||
*
|
||||
* @class AirDCPPSocket
|
||||
* @example
|
||||
* const socket = new AirDCPPSocket({
|
||||
* protocol: "https",
|
||||
* hostname: "localhost:5600",
|
||||
* username: "admin",
|
||||
* password: "password"
|
||||
* });
|
||||
* await socket.connect();
|
||||
*/
|
||||
class AirDCPPSocket {
|
||||
/**
|
||||
* Creates a new AirDC++ socket connection instance.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} configuration - Connection configuration object
|
||||
* @param {string} configuration.protocol - HTTP protocol ("http" or "https")
|
||||
* @param {string} configuration.hostname - Server hostname with optional port
|
||||
* @param {string} configuration.username - AirDC++ username for authentication
|
||||
* @param {string} configuration.password - AirDC++ password for authentication
|
||||
* @returns {Socket} Configured AirDC++ socket instance
|
||||
*/
|
||||
constructor(configuration) {
|
||||
let socketProtocol = "";
|
||||
if (configuration.protocol === "https") {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Search API service for AirDC++ Direct Connect searches.
|
||||
* Provides high-level search functionality by wrapping the socket service
|
||||
* and managing search instances and result collection.
|
||||
* @module services/api/SearchApi
|
||||
*/
|
||||
|
||||
import SocketService from "../DcppSearchService";
|
||||
import {
|
||||
SearchQuery,
|
||||
@@ -7,16 +14,46 @@ import {
|
||||
} from "threetwo-ui-typings";
|
||||
import SearchConstants from "../../constants/search.constants";
|
||||
|
||||
/**
|
||||
* Configuration data for initiating an AirDC++ search.
|
||||
* @interface SearchData
|
||||
* @property {Object} query - Search query configuration
|
||||
* @property {string} query.pattern - Required search pattern/term
|
||||
* @property {string[]|undefined|null} hub_urls - List of hub URLs to search, or null for all hubs
|
||||
* @property {PriorityEnum} priority - Download priority for matched results
|
||||
*/
|
||||
interface SearchData {
|
||||
query: Pick<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
|
||||
hub_urls: string[] | undefined | null;
|
||||
priority: PriorityEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses execution for a specified duration.
|
||||
*
|
||||
* @private
|
||||
* @param {number} ms - Duration to sleep in milliseconds
|
||||
* @returns {Promise} Promise that resolves after the specified delay
|
||||
*/
|
||||
function sleep(ms: number): Promise<NodeJS.Timeout> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a search on the AirDC++ network through connected hubs.
|
||||
* Creates a new search instance, registers listeners for results,
|
||||
* and sends the search query to the specified hubs.
|
||||
*
|
||||
* @async
|
||||
* @param {SearchData} data - Search configuration data
|
||||
* @returns {Promise<Object>} Search queue information from the server
|
||||
* @example
|
||||
* const queueInfo = await search({
|
||||
* query: { pattern: "Batman #1" },
|
||||
* hub_urls: null, // Search all connected hubs
|
||||
* priority: PriorityEnum.Normal
|
||||
* });
|
||||
*/
|
||||
export const search = async (data: SearchData) => {
|
||||
await SocketService.connect();
|
||||
const instance: SearchInstance = await SocketService.post("search");
|
||||
@@ -36,6 +73,17 @@ export const search = async (data: SearchData) => {
|
||||
return searchQueueInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback handler for when a search query has been sent to hubs.
|
||||
* Waits for results to accumulate, then processes the top matches.
|
||||
*
|
||||
* @private
|
||||
* @async
|
||||
* @param {SearchData} item - The original search data
|
||||
* @param {SearchInstance} instance - The search instance object
|
||||
* @param {Function} unsubscribe - Cleanup function to remove the event listener
|
||||
* @param {Object} searchInfo - Information about the sent search
|
||||
*/
|
||||
const onSearchSent = async (item, instance, unsubscribe, searchInfo) => {
|
||||
// Collect the results for 5 seconds
|
||||
await sleep(5000);
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for string formatting and text manipulation.
|
||||
* @module shared/utils/formatting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Removes a leading period character from a string if present.
|
||||
* Useful for normalizing file extensions or dotfile names.
|
||||
*
|
||||
* @param {string} input - The input string to process
|
||||
* @returns {string} The input string with the leading period removed, if one existed
|
||||
* @example
|
||||
* removeLeadingPeriod(".txt") // returns "txt"
|
||||
* removeLeadingPeriod("txt") // returns "txt"
|
||||
*/
|
||||
export const removeLeadingPeriod = (input: string): string => {
|
||||
if (input.charAt(0) == ".") {
|
||||
input = input.substr(1);
|
||||
@@ -5,10 +20,37 @@ export const removeLeadingPeriod = (input: string): string => {
|
||||
return input;
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes pound/hash symbols (#) in a string by replacing them with URL-encoded equivalent (%23).
|
||||
* Useful for preparing strings to be used in URLs or file paths.
|
||||
*
|
||||
* @param {string} input - The input string containing pound symbols to escape
|
||||
* @returns {string} The input string with all # characters replaced with %23
|
||||
* @example
|
||||
* escapePoundSymbol("Issue #123") // returns "Issue %23123"
|
||||
*/
|
||||
export const escapePoundSymbol = (input: string): string => {
|
||||
return input.replace(/\#/gm, "%23");
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a comic book document with various metadata sources.
|
||||
* Used for displaying comic information across the application.
|
||||
*
|
||||
* @interface ComicBookDocument
|
||||
* @property {string} id - Unique identifier for the comic book document
|
||||
* @property {Object} [canonicalMetadata] - User-verified or authoritative metadata
|
||||
* @property {Object} [canonicalMetadata.series] - Series information
|
||||
* @property {string|null} [canonicalMetadata.series.value] - Series name
|
||||
* @property {Object} [canonicalMetadata.issueNumber] - Issue number information
|
||||
* @property {string|number|null} [canonicalMetadata.issueNumber.value] - Issue number
|
||||
* @property {Object} [inferredMetadata] - Automatically detected metadata from file parsing
|
||||
* @property {Object} [inferredMetadata.issue] - Issue information
|
||||
* @property {string|null} [inferredMetadata.issue.name] - Inferred series/issue name
|
||||
* @property {string|number|null} [inferredMetadata.issue.number] - Inferred issue number
|
||||
* @property {Object} [rawFileDetails] - Original file information
|
||||
* @property {string|null} [rawFileDetails.name] - Original filename
|
||||
*/
|
||||
interface ComicBookDocument {
|
||||
id: string;
|
||||
canonicalMetadata?: {
|
||||
@@ -27,8 +69,17 @@ interface ComicBookDocument {
|
||||
* Generates a display label for a comic book from its metadata.
|
||||
* Prioritizes canonical metadata, falls back to inferred, then raw file name.
|
||||
*
|
||||
* @param comic - The comic book document object
|
||||
* @returns A formatted string like "Series Name #123" or the file name as fallback
|
||||
* @param {ComicBookDocument} comic - The comic book document object
|
||||
* @returns {string} A formatted string like "Series Name #123" or the file name as fallback
|
||||
* @example
|
||||
* // With full canonical metadata
|
||||
* getComicDisplayLabel({ id: "1", canonicalMetadata: { series: { value: "Batman" }, issueNumber: { value: 42 } } })
|
||||
* // returns "Batman #42"
|
||||
*
|
||||
* @example
|
||||
* // Fallback to raw file name
|
||||
* getComicDisplayLabel({ id: "1", rawFileDetails: { name: "batman-042.cbz" } })
|
||||
* // returns "batman-042.cbz"
|
||||
*/
|
||||
export const getComicDisplayLabel = (comic: ComicBookDocument): string => {
|
||||
const series =
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
// i18n.js
|
||||
/**
|
||||
* @fileoverview Internationalization (i18n) configuration module.
|
||||
* Sets up i18next with HTTP backend for loading translations,
|
||||
* automatic language detection, and React integration.
|
||||
* @module shared/utils/i18n
|
||||
* @see {@link https://www.i18next.com/overview/configuration-options i18next Configuration Options}
|
||||
*/
|
||||
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import HttpBackend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
/**
|
||||
* Configured i18next instance for the application.
|
||||
*
|
||||
* Features:
|
||||
* - HTTP backend for loading translation files
|
||||
* - Automatic browser language detection
|
||||
* - React-i18next integration for hooks and components
|
||||
* - English as default and fallback language
|
||||
*
|
||||
* @type {import("i18next").i18n}
|
||||
* @example
|
||||
* // Using in a component
|
||||
* import { useTranslation } from 'react-i18next';
|
||||
* const { t } = useTranslation();
|
||||
* return <span>{t('key.name')}</span>;
|
||||
*/
|
||||
i18n
|
||||
// Learn more about options: https://www.i18next.com/overview/configuration-options
|
||||
.use(HttpBackend) // Load translations over http
|
||||
.use(LanguageDetector) // Detect language automatically
|
||||
.use(initReactI18next) // Pass i18n instance to react-i18next
|
||||
.use(HttpBackend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: "en", // Specify the language
|
||||
/** @type {string} Default language code */
|
||||
lng: "en",
|
||||
/** @type {string} Fallback language when translation is missing */
|
||||
fallbackLng: "en",
|
||||
/** @type {boolean} Enable debug mode for development */
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false, // Not needed for React
|
||||
/** @type {boolean} Disable escaping since React handles XSS protection */
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
// path where resources get loaded from
|
||||
/** @type {string} Path to translation JSON files */
|
||||
loadPath: "./src/client/locales/en/translation.json",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,17 +1,59 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for handling comic metadata from various sources.
|
||||
* Provides functions to determine cover images and external metadata from
|
||||
* sources like ComicVine, League of Comic Geeks (LOCG), and raw file details.
|
||||
* @module shared/utils/metadata
|
||||
*/
|
||||
|
||||
import { filter, isEmpty, isNil, isUndefined, min, minBy } from "lodash";
|
||||
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";
|
||||
import { escapePoundSymbol } from "./formatting.utils";
|
||||
|
||||
/**
|
||||
* @typedef {Object} CoverFileEntry
|
||||
* @property {string} objectReference - Reference key to the source object
|
||||
* @property {number} priority - Priority level for cover selection (lower = higher priority)
|
||||
* @property {string} url - URL to the cover image
|
||||
* @property {string} issueName - Name of the comic issue
|
||||
* @property {string} publisher - Publisher name
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ComicMetadataPayload
|
||||
* @property {Object} [rawFileDetails] - Raw file information from the filesystem
|
||||
* @property {Object} [rawFileDetails.cover] - Cover image details
|
||||
* @property {string} [rawFileDetails.cover.filePath] - Path to the cover file
|
||||
* @property {string} [rawFileDetails.name] - File name
|
||||
* @property {Object} [wanted] - Wanted list metadata
|
||||
* @property {Object} [comicInfo] - ComicInfo.xml metadata
|
||||
* @property {Object} [comicvine] - ComicVine API metadata
|
||||
* @property {Object} [comicvine.image] - Image information
|
||||
* @property {string} [comicvine.image.small_url] - Small cover image URL
|
||||
* @property {string} [comicvine.name] - Issue name from ComicVine
|
||||
* @property {Object} [comicvine.publisher] - Publisher information
|
||||
* @property {string} [comicvine.publisher.name] - Publisher name
|
||||
* @property {Object} [locg] - League of Comic Geeks metadata
|
||||
* @property {string} [locg.cover] - Cover image URL
|
||||
* @property {string} [locg.name] - Issue name
|
||||
* @property {string} [locg.publisher] - Publisher name
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines the best available cover file from multiple metadata sources.
|
||||
* Evaluates sources in priority order: rawFileDetails (1), wanted (2), comicvine (3), locg (4).
|
||||
* Returns the highest priority source that has a valid cover URL.
|
||||
*
|
||||
* @param {ComicMetadataPayload} data - The comic metadata object containing multiple sources
|
||||
* @returns {CoverFileEntry} The cover file entry with the highest priority that has a URL,
|
||||
* or the rawFile entry if no covers are available
|
||||
* @example
|
||||
* const cover = determineCoverFile({
|
||||
* rawFileDetails: { name: "Batman #1.cbz", cover: { filePath: "covers/batman-1.jpg" } },
|
||||
* comicvine: { image: { small_url: "https://comicvine.com/..." }, name: "Batman" }
|
||||
* });
|
||||
* // Returns rawFileDetails cover (priority 1) if available
|
||||
*/
|
||||
export const determineCoverFile = (data): any => {
|
||||
/* For a payload like this:
|
||||
const foo = {
|
||||
rawFileDetails: {}, // #1
|
||||
wanted: {},
|
||||
comicInfo: {},
|
||||
comicvine: {}, // #2
|
||||
locg: {}, // #3
|
||||
};
|
||||
*/
|
||||
const coverFile = {
|
||||
rawFile: {
|
||||
objectReference: "rawFileDetails",
|
||||
@@ -42,13 +84,15 @@ export const determineCoverFile = (data): any => {
|
||||
publisher: "",
|
||||
},
|
||||
};
|
||||
// comicvine
|
||||
|
||||
// Extract ComicVine metadata
|
||||
if (!isEmpty(data.comicvine)) {
|
||||
coverFile.comicvine.url = data?.comicvine?.image?.small_url;
|
||||
coverFile.comicvine.issueName = data.comicvine?.name;
|
||||
coverFile.comicvine.publisher = data.comicvine?.publisher?.name;
|
||||
}
|
||||
// rawFileDetails
|
||||
|
||||
// Extract raw file details
|
||||
if (!isEmpty(data.rawFileDetails) && data.rawFileDetails.cover?.filePath) {
|
||||
const encodedFilePath = encodeURI(
|
||||
`${LIBRARY_SERVICE_HOST}/${data.rawFileDetails.cover.filePath}`,
|
||||
@@ -58,8 +102,8 @@ export const determineCoverFile = (data): any => {
|
||||
} else if (!isEmpty(data.rawFileDetails)) {
|
||||
coverFile.rawFile.issueName = data.rawFileDetails.name;
|
||||
}
|
||||
// wanted
|
||||
|
||||
// Extract League of Comic Geeks metadata
|
||||
if (!isNil(data.locg)) {
|
||||
coverFile.locg.url = data.locg.cover;
|
||||
coverFile.locg.issueName = data.locg.name;
|
||||
@@ -79,6 +123,28 @@ export const determineCoverFile = (data): any => {
|
||||
return coverFile.rawFile;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} ExternalMetadataResult
|
||||
* @property {string} coverURL - URL to the cover image
|
||||
* @property {string} issue - Issue name or title
|
||||
* @property {string} icon - Icon filename for the metadata source
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts external metadata from a specific source.
|
||||
* Supports ComicVine and League of Comic Geeks (LOCG) as metadata sources.
|
||||
*
|
||||
* @param {string} metadataSource - The source identifier ("comicvine" or "locg")
|
||||
* @param {ComicMetadataPayload} source - The comic metadata object
|
||||
* @returns {ExternalMetadataResult|Object|null} The extracted metadata with cover URL, issue name,
|
||||
* and source icon; empty object for undefined source;
|
||||
* null if source data is nil
|
||||
* @example
|
||||
* const metadata = determineExternalMetadata("comicvine", {
|
||||
* comicvine: { image: { small_url: "https://..." }, name: "Batman #1" }
|
||||
* });
|
||||
* // Returns { coverURL: "https://...", issue: "Batman #1", icon: "cvlogo.svg" }
|
||||
*/
|
||||
export const determineExternalMetadata = (
|
||||
metadataSource: string,
|
||||
source: any,
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for deep object comparison and traversal.
|
||||
* Provides tools for finding differences between objects and recursively
|
||||
* traversing nested object structures.
|
||||
* @module shared/utils/object
|
||||
*/
|
||||
|
||||
import { transform, isEqual, isObject } from "lodash";
|
||||
|
||||
/**
|
||||
* Deep diff between two object, using lodash
|
||||
* @param {Object} object Object compared
|
||||
* @param {Object} base Object to compare with
|
||||
* @return {Object} Return a new object who represent the diff
|
||||
* Calculates the deep difference between two objects.
|
||||
* Returns a new object containing only the properties that differ between
|
||||
* the compared object and the base object.
|
||||
*
|
||||
* @param {Object} object - The object to compare
|
||||
* @param {Object} base - The base object to compare against
|
||||
* @returns {Object} A new object representing the differences, where each key
|
||||
* contains the value from `object` that differs from `base`
|
||||
* @example
|
||||
* const obj1 = { a: 1, b: { c: 2, d: 3 } };
|
||||
* const obj2 = { a: 1, b: { c: 2, d: 4 } };
|
||||
* difference(obj1, obj2); // returns { b: { d: 3 } }
|
||||
*/
|
||||
export const difference = (object, base) => {
|
||||
return changes(object, base);
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal recursive function that computes changes between two objects.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} object - The object to compare
|
||||
* @param {Object} base - The base object to compare against
|
||||
* @returns {Object} Object containing the differences
|
||||
*/
|
||||
const changes = (object, base) => {
|
||||
return transform(object, (result, value, key) => {
|
||||
if (!isEqual(value, base[key])) {
|
||||
@@ -21,6 +44,17 @@ const changes = (object, base) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function type for object traversal.
|
||||
* Called for each property encountered during traversal.
|
||||
*
|
||||
* @template T - The type of the object being traversed
|
||||
* @callback TraverseFunction
|
||||
* @param {T} obj - The root object being traversed
|
||||
* @param {string} prop - The current property name
|
||||
* @param {unknown} value - The value of the current property
|
||||
* @param {string[]} scope - Array of property names representing the path to current property
|
||||
*/
|
||||
export type TraverseFunction<T> = (
|
||||
obj: T,
|
||||
prop: string,
|
||||
@@ -29,16 +63,39 @@ export type TraverseFunction<T> = (
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Deep diff between two object, using lodash
|
||||
* @param {Object} object Object to traverse
|
||||
* @param {Object} fn Callback function
|
||||
* @return {Object} Return a new object who represent the diff
|
||||
* Recursively traverses all properties of an object, invoking a callback
|
||||
* for each property encountered. Useful for deep inspection or transformation
|
||||
* of nested object structures.
|
||||
*
|
||||
* @template T - The type of the object being traversed
|
||||
* @param {T} object - The object to traverse
|
||||
* @param {TraverseFunction<T>} fn - Callback function invoked for each property
|
||||
* @returns {void}
|
||||
* @example
|
||||
* const obj = { a: 1, b: { c: 2 } };
|
||||
* traverseObject(obj, (obj, prop, value, scope) => {
|
||||
* console.log(`${scope.join('.')}.${prop} = ${value}`);
|
||||
* });
|
||||
* // Logs:
|
||||
* // ".a = 1"
|
||||
* // ".b = [object Object]"
|
||||
* // "b.c = 2"
|
||||
*/
|
||||
export const traverseObject = <T = Record<string, unknown>>(
|
||||
object: T,
|
||||
fn: TraverseFunction<T>,
|
||||
): void => traverseInternal(object, fn, []);
|
||||
|
||||
/**
|
||||
* Internal recursive implementation for object traversal.
|
||||
*
|
||||
* @private
|
||||
* @template T - The type of the object being traversed
|
||||
* @param {T} object - The object to traverse
|
||||
* @param {TraverseFunction<T>} fn - Callback function invoked for each property
|
||||
* @param {string[]} [scope=[]] - Current path scope in the object hierarchy
|
||||
* @returns {void}
|
||||
*/
|
||||
const traverseInternal = <T = Record<string, unknown>>(
|
||||
object: T,
|
||||
fn: TraverseFunction<T>,
|
||||
|
||||
@@ -1,12 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for detecting comic book issue types.
|
||||
* Analyzes deck/description text to identify trade paperbacks, mini-series,
|
||||
* and other collected edition formats.
|
||||
* @module shared/utils/tradepaperback
|
||||
*/
|
||||
|
||||
import { flatten, compact, map, isEmpty, isNil } from "lodash";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* @typedef {Object} IssueTypeMatcher
|
||||
* @property {RegExp[]} regex - Array of regex patterns to match against the deck text
|
||||
* @property {string} displayName - Human-readable name for the issue type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IssueTypeMatch
|
||||
* @property {string} displayName - The display name of the matched issue type
|
||||
* @property {string[]} results - Array of matched strings from the deck text
|
||||
*/
|
||||
|
||||
/**
|
||||
* Detects the type of comic issue from its deck/description text.
|
||||
* Identifies formats like Trade Paperbacks (TPB), hardcover collections,
|
||||
* and mini-series based on keyword patterns.
|
||||
*
|
||||
* @param {string} deck - The deck or description text to analyze
|
||||
* @returns {IssueTypeMatch|undefined} The first matched issue type with its display name
|
||||
* and matched strings, or undefined if no match
|
||||
* @example
|
||||
* detectIssueTypes("Collects issues #1-6 in trade paperback format")
|
||||
* // returns { displayName: "Trade Paperback", results: ["trade paperback"] }
|
||||
*
|
||||
* @example
|
||||
* detectIssueTypes("A 4-issue mini-series")
|
||||
* // returns { displayName: "Mini-Series", results: ["mini-series"] }
|
||||
*/
|
||||
export const detectIssueTypes = (deck: string): any => {
|
||||
const issueTypeMatchers = [
|
||||
{
|
||||
regex: [
|
||||
/((trade)?\s?(paperback)|(tpb))/gim, // https://regex101.com/r/FhuowT/1
|
||||
/(hard\s?cover)\s?(collect((ion)|(ed)|(ing)))/gim, //https://regex101.com/r/eFJVRM/1
|
||||
/(hard\s?cover)\s?(collect((ion)|(ed)|(ing)))/gim, // https://regex101.com/r/eFJVRM/1
|
||||
],
|
||||
displayName: "Trade Paperback",
|
||||
},
|
||||
@@ -20,6 +55,16 @@ export const detectIssueTypes = (deck: string): any => {
|
||||
return compact(matches)[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests a deck string against regex patterns and returns match info if found.
|
||||
*
|
||||
* @private
|
||||
* @param {string} deck - The deck or description text to search
|
||||
* @param {RegExp[]} regexPattern - Array of regex patterns to test against
|
||||
* @param {string} displayName - The display name to return if a match is found
|
||||
* @returns {IssueTypeMatch|undefined} Object with displayName and results if matched,
|
||||
* otherwise undefined
|
||||
*/
|
||||
const getIssueTypeDisplayName = (
|
||||
deck: string,
|
||||
regexPattern: RegExp[],
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
/**
|
||||
* @fileoverview Validation utility functions for form inputs and data.
|
||||
* Provides validators for hostnames, URLs, and other common input types.
|
||||
* @module shared/utils/validator
|
||||
*/
|
||||
|
||||
import { isNil, isUndefined } from "lodash";
|
||||
|
||||
/**
|
||||
* Validates a hostname string against RFC 1123 standards.
|
||||
* Hostnames must start and end with alphanumeric characters and can contain
|
||||
* hyphens. Each label (segment between dots) can be 1-63 characters.
|
||||
*
|
||||
* @param {string} hostname - The hostname string to validate
|
||||
* @returns {string|undefined} undefined if valid, error message string if invalid
|
||||
* @see {@link https://stackoverflow.com/a/3824105/656708 Hostname validation regex}
|
||||
* @example
|
||||
* hostNameValidator("example.com") // returns undefined (valid)
|
||||
* hostNameValidator("my-server.local") // returns undefined (valid)
|
||||
* hostNameValidator("-invalid.com") // returns "Enter a valid hostname"
|
||||
* hostNameValidator("invalid-.com") // returns "Enter a valid hostname"
|
||||
*/
|
||||
export const hostNameValidator = (hostname: string): string | undefined => {
|
||||
// https://stackoverflow.com/a/3824105/656708
|
||||
const hostnameRegex =
|
||||
/^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/;
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Global Zustand store for application state management.
|
||||
* Manages socket connections, import job tracking, and ComicVine scraping state.
|
||||
* Uses Zustand for lightweight, hook-based state management with React.
|
||||
* @module store
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
import io, { Socket } from "socket.io-client";
|
||||
import { SOCKET_BASE_URI } from "../constants/endpoints";
|
||||
@@ -5,10 +12,17 @@ import { QueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
/**
|
||||
* React Query client instance for managing server state.
|
||||
* @constant {QueryClient}
|
||||
*/
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
/**
|
||||
* Global application state interface
|
||||
* Global application state interface.
|
||||
* Defines the shape of the Zustand store including socket management,
|
||||
* import job tracking, and external service state.
|
||||
* @interface StoreState
|
||||
*/
|
||||
interface StoreState {
|
||||
/** Active socket.io connections by namespace */
|
||||
|
||||
46
src/client/types/index.d.ts
vendored
46
src/client/types/index.d.ts
vendored
@@ -1,13 +1,55 @@
|
||||
/**
|
||||
* @fileoverview TypeScript type declarations for module imports.
|
||||
* Provides type definitions for importing static assets and stylesheets
|
||||
* that aren't natively supported by TypeScript.
|
||||
* @module types
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module declaration for PNG image imports.
|
||||
* Allows importing .png files as string URLs.
|
||||
* @example
|
||||
* import logo from './assets/logo.png';
|
||||
* // logo is typed as string
|
||||
*/
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module declaration for JPG image imports.
|
||||
* Allows importing .jpg files.
|
||||
*/
|
||||
declare module "*.jpg";
|
||||
|
||||
/**
|
||||
* Module declaration for GIF image imports.
|
||||
* Allows importing .gif files.
|
||||
*/
|
||||
declare module "*.gif";
|
||||
|
||||
/**
|
||||
* Module declaration for LESS stylesheet imports.
|
||||
* Allows importing .less files.
|
||||
*/
|
||||
declare module "*.less";
|
||||
|
||||
/**
|
||||
* Module declaration for SCSS stylesheet imports.
|
||||
* Allows importing .scss files.
|
||||
*/
|
||||
declare module "*.scss";
|
||||
|
||||
/**
|
||||
* Module declaration for CSS stylesheet imports.
|
||||
* Allows importing .css files.
|
||||
*/
|
||||
declare module "*.css";
|
||||
|
||||
// Comic types are now generated from GraphQL schema
|
||||
// Import from '../../graphql/generated' instead
|
||||
/**
|
||||
* @note Comic types are now generated from GraphQL schema.
|
||||
* Import from '../../graphql/generated' instead of defining types here.
|
||||
* The generated types provide full type safety for GraphQL queries and mutations.
|
||||
* @see {@link module:graphql/generated}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user