🛠 changed import to account for graphql
This commit is contained in:
@@ -5,8 +5,7 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
|
import { SearchQuery, PriorityEnum, SearchResponse, SearchInstance } from "threetwo-ui-typings";
|
||||||
import { RootState, SearchInstance } from "threetwo-ui-typings";
|
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { Form, Field } from "react-final-form";
|
import { Form, Field } from "react-final-form";
|
||||||
import { difference } from "../../shared/utils/object.utils";
|
import { difference } from "../../shared/utils/object.utils";
|
||||||
@@ -15,44 +14,104 @@ import { useStore } from "../../store";
|
|||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { AIRDCPP_SERVICE_BASE_URI } from "../../constants/endpoints";
|
import { AIRDCPP_SERVICE_BASE_URI, SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
interface IAcquisitionPanelProps {
|
interface IAcquisitionPanelProps {
|
||||||
query: any;
|
query: any;
|
||||||
comicObjectId: any;
|
comicObjectId: string;
|
||||||
comicObject: any;
|
comicObject: any;
|
||||||
settings: any;
|
settings: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AirDCPPConfig {
|
||||||
|
protocol: string;
|
||||||
|
hostname: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: {
|
||||||
|
id: string;
|
||||||
|
str: string;
|
||||||
|
};
|
||||||
|
size: number;
|
||||||
|
slots: {
|
||||||
|
total: number;
|
||||||
|
free: number;
|
||||||
|
};
|
||||||
|
users: {
|
||||||
|
user: {
|
||||||
|
nicks: string;
|
||||||
|
flags: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
dupe?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchInstanceData {
|
||||||
|
id: number;
|
||||||
|
owner: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchInfo {
|
||||||
|
query: {
|
||||||
|
pattern: string;
|
||||||
|
extensions: string[];
|
||||||
|
file_type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Hub {
|
||||||
|
hub_url: string;
|
||||||
|
identity: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchFormValues {
|
||||||
|
issueName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const AcquisitionPanel = (
|
export const AcquisitionPanel = (
|
||||||
props: IAcquisitionPanelProps,
|
props: IAcquisitionPanelProps,
|
||||||
): ReactElement => {
|
): ReactElement => {
|
||||||
const socketRef = useRef<Socket>();
|
const socketRef = useRef<Socket>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const [dcppQuery, setDcppQuery] = useState({});
|
const [dcppQuery, setDcppQuery] = useState<SearchQuery | null>(null);
|
||||||
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<any[]>([]);
|
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<SearchResult[]>([]);
|
||||||
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
|
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
|
||||||
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState<any>({});
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState<any>({});
|
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState<SearchInstanceData | null>(null);
|
||||||
|
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState<SearchInfo | null>(null);
|
||||||
|
const [searchError, setSearchError] = useState<string | null>(null);
|
||||||
|
|
||||||
const { comicObjectId } = props;
|
const { comicObjectId } = props;
|
||||||
const issueName = props.query.issue.name || "";
|
const issueName = props.query.issue.name || "";
|
||||||
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
||||||
|
|
||||||
|
// Search timeout duration in milliseconds (30 seconds)
|
||||||
|
const SEARCH_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = useStore.getState().getSocket("manual");
|
const socket = useStore.getState().getSocket("manual");
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
|
||||||
// --- Handlers ---
|
// --- Handlers ---
|
||||||
const handleResultAdded = ({ result }: any) => {
|
const handleResultAdded = ({ result }: { result: SearchResult }) => {
|
||||||
setAirDCPPSearchResults((prev) =>
|
setAirDCPPSearchResults((prev) =>
|
||||||
prev.some((r) => r.id === result.id) ? prev : [...prev, result],
|
prev.some((r) => r.id === result.id) ? prev : [...prev, result],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResultUpdated = ({ result }: any) => {
|
const handleResultUpdated = ({ result }: { result: SearchResult }) => {
|
||||||
setAirDCPPSearchResults((prev) => {
|
setAirDCPPSearchResults((prev) => {
|
||||||
const idx = prev.findIndex((r) => r.id === result.id);
|
const idx = prev.findIndex((r) => r.id === result.id);
|
||||||
if (idx === -1) return prev;
|
if (idx === -1) return prev;
|
||||||
@@ -63,45 +122,86 @@ export const AcquisitionPanel = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchInitiated = (data: any) => {
|
const handleSearchInitiated = (data: { instance: SearchInstanceData }) => {
|
||||||
setAirDCPPSearchInstance(data.instance);
|
setAirDCPPSearchInstance(data.instance);
|
||||||
|
setIsSearching(true);
|
||||||
|
setSearchError(null);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timeout to stop searching after SEARCH_TIMEOUT_MS
|
||||||
|
searchTimeoutRef.current = setTimeout(() => {
|
||||||
|
setIsSearching(false);
|
||||||
|
console.log(`Search timeout reached after ${SEARCH_TIMEOUT_MS / 1000} seconds`);
|
||||||
|
}, SEARCH_TIMEOUT_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchesSent = (data: any) => {
|
const handleSearchesSent = (data: { searchInfo: SearchInfo }) => {
|
||||||
setAirDCPPSearchInfo(data.searchInfo);
|
setAirDCPPSearchInfo(data.searchInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchError = (error: { message: string }) => {
|
||||||
|
setSearchError(error.message || "Search failed");
|
||||||
|
setIsSearching(false);
|
||||||
|
|
||||||
|
// Clear timeout on error
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
searchTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchCompleted = () => {
|
||||||
|
setIsSearching(false);
|
||||||
|
|
||||||
|
// Clear timeout when search completes
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
searchTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Subscribe once ---
|
// --- Subscribe once ---
|
||||||
socket.on("searchResultAdded", handleResultAdded);
|
socket.on("searchResultAdded", handleResultAdded);
|
||||||
socket.on("searchResultUpdated", handleResultUpdated);
|
socket.on("searchResultUpdated", handleResultUpdated);
|
||||||
socket.on("searchInitiated", handleSearchInitiated);
|
socket.on("searchInitiated", handleSearchInitiated);
|
||||||
socket.on("searchesSent", handleSearchesSent);
|
socket.on("searchesSent", handleSearchesSent);
|
||||||
|
socket.on("searchError", handleSearchError);
|
||||||
|
socket.on("searchCompleted", handleSearchCompleted);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("searchResultAdded", handleResultAdded);
|
socket.off("searchResultAdded", handleResultAdded);
|
||||||
socket.off("searchResultUpdated", handleResultUpdated);
|
socket.off("searchResultUpdated", handleResultUpdated);
|
||||||
socket.off("searchInitiated", handleSearchInitiated);
|
socket.off("searchInitiated", handleSearchInitiated);
|
||||||
socket.off("searchesSent", handleSearchesSent);
|
socket.off("searchesSent", handleSearchesSent);
|
||||||
// if you want to fully close the socket:
|
socket.off("searchError", handleSearchError);
|
||||||
// useStore.getState().disconnectSocket("/manual");
|
socket.off("searchCompleted", handleSearchCompleted);
|
||||||
|
|
||||||
|
// Clean up timeout on unmount
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [SEARCH_TIMEOUT_MS]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: settings,
|
data: settings,
|
||||||
isLoading,
|
isLoading: isLoadingSettings,
|
||||||
isError,
|
isError: isSettingsError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["settings"],
|
queryKey: ["settings"],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await axios({
|
await axios({
|
||||||
url: "http://localhost:3000/api/settings/getAllSettings",
|
url: `${SETTINGS_SERVICE_BASE_URI}/getAllSettings`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: hubs } = useQuery({
|
const { data: hubs, isLoading: isLoadingHubs } = useQuery({
|
||||||
queryKey: ["hubs"],
|
queryKey: ["hubs", settings?.data.directConnect?.client?.host],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await axios({
|
await axios({
|
||||||
url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`,
|
url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`,
|
||||||
@@ -110,45 +210,72 @@ export const AcquisitionPanel = (
|
|||||||
host: settings?.data.directConnect?.client?.host,
|
host: settings?.data.directConnect?.client?.host,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
enabled: !isEmpty(settings?.data.directConnect?.client?.host),
|
enabled: !!settings?.data?.directConnect?.client?.host,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get AirDC++ config from settings
|
||||||
|
const airDCPPConfig: AirDCPPConfig | null = settings?.data?.directConnect?.client
|
||||||
|
? {
|
||||||
|
protocol: settings.data.directConnect.client.protocol || "ws",
|
||||||
|
hostname: typeof settings.data.directConnect.client.host === 'string'
|
||||||
|
? settings.data.directConnect.client.host
|
||||||
|
: `${settings.data.directConnect.client.host?.hostname || 'localhost'}:${settings.data.directConnect.client.host?.port || '5600'}`,
|
||||||
|
username: settings.data.directConnect.client.username || "admin",
|
||||||
|
password: settings.data.directConnect.client.password || "password",
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dcppSearchQuery = {
|
if (hubs?.data && Array.isArray(hubs.data) && hubs.data.length > 0) {
|
||||||
query: {
|
const dcppSearchQuery = {
|
||||||
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
|
query: {
|
||||||
extensions: ["cbz", "cbr", "cb7"],
|
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
|
||||||
},
|
extensions: ["cbz", "cbr", "cb7"],
|
||||||
hub_urls: map(hubs?.data, (item) => item.value),
|
},
|
||||||
priority: 5,
|
hub_urls: map(hubs.data, (item) => item.value),
|
||||||
};
|
priority: 5,
|
||||||
setDcppQuery(dcppSearchQuery);
|
};
|
||||||
|
setDcppQuery(dcppSearchQuery as any);
|
||||||
|
}
|
||||||
}, [hubs, sanitizedIssueName]);
|
}, [hubs, sanitizedIssueName]);
|
||||||
|
|
||||||
const search = async (searchData: any) => {
|
const search = async (searchData: any) => {
|
||||||
|
if (!airDCPPConfig) {
|
||||||
|
setSearchError("AirDC++ configuration not found in settings");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!socketRef.current) {
|
||||||
|
setSearchError("Socket connection not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setAirDCPPSearchResults([]);
|
setAirDCPPSearchResults([]);
|
||||||
socketRef.current?.emit("call", "socket.search", {
|
setIsSearching(true);
|
||||||
|
setSearchError(null);
|
||||||
|
|
||||||
|
socketRef.current.emit("call", "socket.search", {
|
||||||
query: searchData,
|
query: searchData,
|
||||||
namespace: "/manual",
|
namespace: "/manual",
|
||||||
config: {
|
config: airDCPPConfig,
|
||||||
protocol: `ws`,
|
|
||||||
hostname: `192.168.1.119:5600`,
|
|
||||||
username: `admin`,
|
|
||||||
password: `password`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = async (
|
const download = async (
|
||||||
searchInstanceId: Number,
|
searchInstanceId: number,
|
||||||
resultId: String,
|
resultId: string,
|
||||||
comicObjectId: String,
|
comicObjectId: string,
|
||||||
name: String,
|
name: string,
|
||||||
size: Number,
|
size: number,
|
||||||
type: any,
|
type: SearchResult["type"],
|
||||||
config: any,
|
config: AirDCPPConfig,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
socketRef.current?.emit(
|
if (!socketRef.current) {
|
||||||
|
console.error("Socket connection not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketRef.current.emit(
|
||||||
"call",
|
"call",
|
||||||
"socket.download",
|
"socket.download",
|
||||||
{
|
{
|
||||||
@@ -160,17 +287,27 @@ export const AcquisitionPanel = (
|
|||||||
type,
|
type,
|
||||||
config,
|
config,
|
||||||
},
|
},
|
||||||
(data: any) => console.log(data),
|
(data: any) => console.log("Download initiated:", data),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDCPPSearchResults = async (searchQuery) => {
|
const getDCPPSearchResults = async (searchQuery: SearchFormValues) => {
|
||||||
|
if (!searchQuery.issueName || searchQuery.issueName.trim() === "") {
|
||||||
|
setSearchError("Please enter a search term");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hubs?.data || !Array.isArray(hubs.data) || hubs.data.length === 0) {
|
||||||
|
setSearchError("No hubs configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const manualQuery = {
|
const manualQuery = {
|
||||||
query: {
|
query: {
|
||||||
pattern: `${searchQuery.issueName}`,
|
pattern: `${searchQuery.issueName.trim()}`,
|
||||||
extensions: ["cbz", "cbr", "cb7"],
|
extensions: ["cbz", "cbr", "cb7"],
|
||||||
},
|
},
|
||||||
hub_urls: [hubs?.data[0].hub_url],
|
hub_urls: [hubs.data[0].hub_url],
|
||||||
priority: 5,
|
priority: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,7 +317,12 @@ export const AcquisitionPanel = (
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-5 mb-3">
|
<div className="mt-5 mb-3">
|
||||||
{!isEmpty(hubs?.data) ? (
|
{isLoadingSettings || isLoadingHubs ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<i className="icon-[solar--refresh-bold-duotone] h-5 w-5 animate-spin" />
|
||||||
|
Loading configuration...
|
||||||
|
</div>
|
||||||
|
) : !isEmpty(hubs?.data) ? (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={getDCPPSearchResults}
|
onSubmit={getDCPPSearchResults}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@@ -200,20 +342,31 @@ export const AcquisitionPanel = (
|
|||||||
{...input}
|
{...input}
|
||||||
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
placeholder="Type an issue/volume name"
|
placeholder="Type an issue/volume name"
|
||||||
|
disabled={isSearching}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="sm:mt-0 min-w-fit rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
className="sm:mt-0 min-w-fit rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={isSearching}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row items-center">
|
||||||
Search DC++
|
{isSearching ? (
|
||||||
<div className="h-5 w-5 ml-2">
|
<>
|
||||||
<img
|
<i className="icon-[solar--refresh-bold-duotone] h-5 w-5 animate-spin mr-2" />
|
||||||
src="/src/client/assets/img/airdcpp_logo.svg"
|
Searching...
|
||||||
className="h-5 w-5"
|
</>
|
||||||
/>
|
) : (
|
||||||
</div>
|
<>
|
||||||
|
Search DC++
|
||||||
|
<div className="h-5 w-5 ml-2">
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/airdcpp_logo.svg"
|
||||||
|
className="h-5 w-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,26 +387,36 @@ export const AcquisitionPanel = (
|
|||||||
</article>
|
</article>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search Error Display */}
|
||||||
|
{searchError && (
|
||||||
|
<article
|
||||||
|
role="alert"
|
||||||
|
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-red-500 bg-red-50 p-4 dark:border-s-4 dark:border-red-600 dark:bg-red-300 dark:text-slate-600"
|
||||||
|
>
|
||||||
|
<strong>Error:</strong> {searchError}
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
{/* configured hub */}
|
{/* configured hub */}
|
||||||
{!isEmpty(hubs?.data) && (
|
{!isEmpty(hubs?.data) && hubs?.data[0] && (
|
||||||
<span className="inline-flex items-center bg-green-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-green-300">
|
<span className="inline-flex items-center bg-green-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-green-300">
|
||||||
<span className="pr-1 pt-1">
|
<span className="pr-1 pt-1">
|
||||||
<i className="icon-[solar--server-2-bold-duotone] w-5 h-5"></i>
|
<i className="icon-[solar--server-2-bold-duotone] w-5 h-5"></i>
|
||||||
</span>
|
</span>
|
||||||
{hubs && hubs?.data[0].hub_url}
|
{hubs.data[0].hub_url}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AirDC++ search instance details */}
|
{/* AirDC++ search instance details */}
|
||||||
{!isNil(airDCPPSearchInstance) &&
|
{airDCPPSearchInstance &&
|
||||||
!isEmpty(airDCPPSearchInfo) &&
|
airDCPPSearchInfo &&
|
||||||
!isNil(hubs) && (
|
hubs?.data && (
|
||||||
<div className="flex flex-row gap-3 my-5 font-hasklig">
|
<div className="flex flex-row gap-3 my-5 font-hasklig">
|
||||||
<div className="block max-w-sm h-fit p-6 text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
<div className="block max-w-sm h-fit p-6 text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{hubs?.data.map((value, idx: string) => (
|
{hubs.data.map((value: Hub, idx: number) => (
|
||||||
<span className="tag is-warning" key={idx}>
|
<span className="tag is-warning" key={idx}>
|
||||||
{value.identity.name}
|
{value.identity.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -293,7 +456,7 @@ export const AcquisitionPanel = (
|
|||||||
|
|
||||||
{/* AirDC++ results */}
|
{/* AirDC++ results */}
|
||||||
<div className="">
|
<div className="">
|
||||||
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
|
{airDCPPSearchResults.length > 0 ? (
|
||||||
<div className="overflow-x-auto max-w-full mt-6">
|
<div className="overflow-x-auto max-w-full mt-6">
|
||||||
<table className="w-full table-auto text-sm text-gray-900 dark:text-slate-100">
|
<table className="w-full table-auto text-sm text-gray-900 dark:text-slate-100">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -345,9 +508,9 @@ export const AcquisitionPanel = (
|
|||||||
<i className="icon-[solar--user-rounded-bold-duotone] w-4 h-4"></i>
|
<i className="icon-[solar--user-rounded-bold-duotone] w-4 h-4"></i>
|
||||||
{users.user.nicks}
|
{users.user.nicks}
|
||||||
</span>
|
</span>
|
||||||
{users.user.flags.map((flag, idx) => (
|
{users.user.flags.map((flag: string, flagIdx: number) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={flagIdx}
|
||||||
className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900"
|
className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900"
|
||||||
>
|
>
|
||||||
<i className="icon-[solar--tag-horizontal-bold-duotone] w-4 h-4"></i>
|
<i className="icon-[solar--tag-horizontal-bold-duotone] w-4 h-4"></i>
|
||||||
@@ -378,23 +541,21 @@ export const AcquisitionPanel = (
|
|||||||
{/* ACTIONS */}
|
{/* ACTIONS */}
|
||||||
<td className="px-2 py-3">
|
<td className="px-2 py-3">
|
||||||
<button
|
<button
|
||||||
className="inline-flex items-center gap-1 rounded border border-green-500 bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-transparent hover:text-green-400 dark:border-green-300 dark:bg-green-300 dark:text-slate-900 dark:hover:bg-transparent"
|
className="inline-flex items-center gap-1 rounded border border-green-500 bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-transparent hover:text-green-400 dark:border-green-300 dark:bg-green-300 dark:text-slate-900 dark:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
download(
|
if (airDCPPSearchInstance && airDCPPConfig) {
|
||||||
airDCPPSearchInstance.id,
|
download(
|
||||||
id,
|
airDCPPSearchInstance.id,
|
||||||
comicObjectId,
|
id,
|
||||||
name,
|
comicObjectId,
|
||||||
size,
|
name,
|
||||||
type,
|
size,
|
||||||
{
|
type,
|
||||||
protocol: `ws`,
|
airDCPPConfig,
|
||||||
hostname: `192.168.1.119:5600`,
|
);
|
||||||
username: `admin`,
|
}
|
||||||
password: `password`,
|
}}
|
||||||
},
|
disabled={!airDCPPSearchInstance || !airDCPPConfig}
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
<i className="icon-[solar--download-bold-duotone] w-4 h-4"></i>
|
<i className="icon-[solar--download-bold-duotone] w-4 h-4"></i>
|
||||||
@@ -406,7 +567,7 @@ export const AcquisitionPanel = (
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : !isSearching ? (
|
||||||
<div className="">
|
<div className="">
|
||||||
<article
|
<article
|
||||||
role="alert"
|
role="alert"
|
||||||
@@ -432,6 +593,11 @@ export const AcquisitionPanel = (
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400 mt-6 p-4">
|
||||||
|
<i className="icon-[solar--refresh-bold-duotone] h-6 w-6 animate-spin" />
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { isEmpty, isNil, isUndefined, map } from "lodash";
|
|||||||
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
||||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||||
import Header from "../shared/Header";
|
import Header from "../shared/Header";
|
||||||
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
|
|
||||||
type WantedComicsListProps = {
|
type WantedComicsListProps = {
|
||||||
comics: any;
|
comics: any;
|
||||||
@@ -16,107 +17,126 @@ export const WantedComicsList = ({
|
|||||||
}: WantedComicsListProps): ReactElement => {
|
}: WantedComicsListProps): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// embla carousel
|
||||||
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||||
|
loop: false,
|
||||||
|
align: "start",
|
||||||
|
containScroll: "trimSnaps",
|
||||||
|
slidesToScroll: 1,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<Header
|
<Header
|
||||||
headerContent="Wanted Comics"
|
headerContent="Wanted Comics"
|
||||||
subHeaderContent="Comics marked as wanted from various sources"
|
subHeaderContent="Comics marked as wanted from various sources"
|
||||||
iconClassNames="fa-solid fa-binoculars mr-2"
|
iconClassNames="fa-solid fa-binoculars mr-2"
|
||||||
link={"/wanted"}
|
link={"/wanted"}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-5 gap-6 mt-3">
|
<div className="overflow-hidden -mr-4 sm:-mr-8 lg:-mr-16 xl:-mr-20 2xl:-mr-24 mt-3">
|
||||||
{map(
|
<div className="overflow-hidden" ref={emblaRef}>
|
||||||
comics,
|
<div className="flex">
|
||||||
({
|
{map(
|
||||||
_id,
|
comics,
|
||||||
rawFileDetails,
|
(
|
||||||
sourcedMetadata: { comicvine, comicInfo, locg },
|
{
|
||||||
wanted,
|
_id,
|
||||||
}) => {
|
rawFileDetails,
|
||||||
const isComicBookMetadataAvailable = !isUndefined(comicvine);
|
sourcedMetadata: { comicvine, comicInfo, locg },
|
||||||
const consolidatedComicMetadata = {
|
wanted,
|
||||||
rawFileDetails,
|
},
|
||||||
comicvine,
|
idx,
|
||||||
comicInfo,
|
) => {
|
||||||
locg,
|
const isComicBookMetadataAvailable = !isUndefined(comicvine);
|
||||||
};
|
const consolidatedComicMetadata = {
|
||||||
|
rawFileDetails,
|
||||||
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueName,
|
issueName,
|
||||||
url,
|
url,
|
||||||
publisher = null,
|
publisher = null,
|
||||||
} = determineCoverFile(consolidatedComicMetadata);
|
} = determineCoverFile(consolidatedComicMetadata);
|
||||||
const titleElement = (
|
const titleElement = (
|
||||||
<Link to={"/comic/details/" + _id}>
|
<Link to={"/comic/details/" + _id}>
|
||||||
{ellipsize(issueName, 20)}
|
{ellipsize(issueName, 20)}
|
||||||
<p>{publisher}</p>
|
<p>{publisher}</p>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Card
|
<div
|
||||||
key={_id}
|
key={idx}
|
||||||
orientation={"vertical-2"}
|
className="flex-[0_0_200px] min-w-0 sm:flex-[0_0_220px] md:flex-[0_0_240px] lg:flex-[0_0_260px] xl:flex-[0_0_280px] pr-[15px]"
|
||||||
imageUrl={url}
|
>
|
||||||
hasDetails
|
<Card
|
||||||
title={issueName ? titleElement : <span>No Name</span>}
|
orientation={"vertical-2"}
|
||||||
>
|
imageUrl={url}
|
||||||
<div className="pb-1">
|
hasDetails
|
||||||
<div className="flex flex-row gap-2">
|
title={issueName ? titleElement : <span>No Name</span>}
|
||||||
{/* Issue type */}
|
>
|
||||||
{isComicBookMetadataAvailable &&
|
<div className="pb-1">
|
||||||
!isNil(detectIssueTypes(comicvine.description)) ? (
|
<div className="flex flex-row gap-2">
|
||||||
<div className="my-2">
|
{/* Issue type */}
|
||||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
{isComicBookMetadataAvailable &&
|
||||||
<span className="pr-1 pt-1">
|
!isNil(detectIssueTypes(comicvine.description)) ? (
|
||||||
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
|
<div className="my-2">
|
||||||
</span>
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
{
|
{
|
||||||
detectIssueTypes(comicvine.description)
|
detectIssueTypes(comicvine.description)
|
||||||
.displayName
|
.displayName
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{/* issues marked as wanted, part of this volume */}
|
||||||
|
{wanted?.markEntireVolumeWanted ? (
|
||||||
|
<div className="text-sm">sagla volume pahije</div>
|
||||||
|
) : (
|
||||||
|
<div className="my-2">
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
|
{wanted.issues.length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* comicVine metadata presence */}
|
||||||
|
{isComicBookMetadataAvailable && (
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/cvlogo.svg"
|
||||||
|
alt={"ComicVine metadata detected."}
|
||||||
|
className="inline-block w-6 h-6 md:w-7 md:h-7 flex-shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isEmpty(locg) && (
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/locglogo.svg"
|
||||||
|
className="w-7 h-7"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</Card>
|
||||||
{/* issues marked as wanted, part of this volume */}
|
|
||||||
{wanted?.markEntireVolumeWanted ? (
|
|
||||||
<div className="text-sm">sagla volume pahije</div>
|
|
||||||
) : (
|
|
||||||
<div className="my-2">
|
|
||||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
|
||||||
<span className="pr-1 pt-1">
|
|
||||||
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
{wanted.issues.length}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* comicVine metadata presence */}
|
);
|
||||||
{isComicBookMetadataAvailable && (
|
},
|
||||||
<img
|
)}
|
||||||
src="/src/client/assets/img/cvlogo.svg"
|
</div>
|
||||||
alt={"ComicVine metadata detected."}
|
</div>
|
||||||
className="w-7 h-7"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isEmpty(locg) && (
|
|
||||||
<img
|
|
||||||
src="/src/client/assets/img/locglogo.svg"
|
|
||||||
className="w-7 h-7"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect } from "react";
|
import React, { ReactElement, useCallback, useEffect, useRef } from "react";
|
||||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import Loader from "react-loader-spinner";
|
import Loader from "react-loader-spinner";
|
||||||
@@ -27,12 +27,21 @@ interface IProps {
|
|||||||
|
|
||||||
export const Import = (props: IProps): ReactElement => {
|
export const Import = (props: IProps): ReactElement => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { importJobQueue, socketIOInstance } = useStore(
|
const { importJobQueue, getSocket, setQueryClientRef } = useStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
importJobQueue: state.importJobQueue,
|
importJobQueue: state.importJobQueue,
|
||||||
socketIOInstance: state.socketIOInstance,
|
getSocket: state.getSocket,
|
||||||
|
setQueryClientRef: state.setQueryClientRef,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const previousResultCountRef = useRef<number>(0);
|
||||||
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Set the queryClient reference in the store so socket events can use it
|
||||||
|
useEffect(() => {
|
||||||
|
setQueryClientRef({ current: queryClient });
|
||||||
|
}, [queryClient, setQueryClientRef]);
|
||||||
|
|
||||||
const sessionId = localStorage.getItem("sessionId");
|
const sessionId = localStorage.getItem("sessionId");
|
||||||
const { mutate: initiateImport } = useMutation({
|
const { mutate: initiateImport } = useMutation({
|
||||||
@@ -44,24 +53,91 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, isError, isLoading } = useQuery({
|
const { data, isError, isLoading, refetch } = useQuery({
|
||||||
queryKey: ["allImportJobResults"],
|
queryKey: ["allImportJobResults"],
|
||||||
queryFn: async () =>
|
queryFn: async () => {
|
||||||
await axios({
|
const response = await axios({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
||||||
}),
|
params: {
|
||||||
|
_t: Date.now(), // Cache buster
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track the result count
|
||||||
|
if (response.data?.length) {
|
||||||
|
previousResultCountRef.current = response.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
staleTime: 0, // Always consider data stale
|
||||||
|
gcTime: 0, // Don't cache the data (replaces cacheTime in newer versions)
|
||||||
|
// Poll every 5 seconds when import is running
|
||||||
|
refetchInterval: importJobQueue.status === "running" || importJobQueue.status === "paused" ? 5000 : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for import queue drained event to refresh the table
|
||||||
|
useEffect(() => {
|
||||||
|
const socket = getSocket("/");
|
||||||
|
|
||||||
|
const handleQueueDrained = () => {
|
||||||
|
const initialCount = previousResultCountRef.current;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 20; // Poll for up to 20 seconds
|
||||||
|
|
||||||
|
// Clear any existing polling interval
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll every second until we see new data or hit max attempts
|
||||||
|
pollingIntervalRef.current = setInterval(async () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const result = await refetch();
|
||||||
|
const newCount = result.data?.data?.length || 0;
|
||||||
|
|
||||||
|
if (newCount > initialCount) {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
} else if (attempts >= maxAttempts) {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [getSocket, queryClient, refetch]);
|
||||||
|
|
||||||
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
||||||
socketIOInstance.emit(
|
const socket = getSocket("/");
|
||||||
|
socket.emit(
|
||||||
"call",
|
"call",
|
||||||
"socket.setQueueStatus",
|
"socket.setQueueStatus",
|
||||||
{
|
{
|
||||||
queueAction,
|
queueAction,
|
||||||
queueStatus,
|
queueStatus,
|
||||||
},
|
},
|
||||||
(data) => console.log(data),
|
(data: any) => console.log(data),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +322,7 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{data?.data.map((jobResult, id) => {
|
{data?.data.map((jobResult: any, id: number) => {
|
||||||
return (
|
return (
|
||||||
<tr key={id}>
|
<tr key={id}>
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const renderCard = (props: ICardProps): ReactElement => {
|
|||||||
<img
|
<img
|
||||||
alt="Home"
|
alt="Home"
|
||||||
src={props.imageUrl}
|
src={props.imageUrl}
|
||||||
className="rounded-t-md object-cover"
|
className="rounded-t-md object-cover w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{props.title ? (
|
{props.title ? (
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ import { create } from "zustand";
|
|||||||
import io, { Socket } from "socket.io-client";
|
import io, { Socket } from "socket.io-client";
|
||||||
import { SOCKET_BASE_URI } from "../constants/endpoints";
|
import { SOCKET_BASE_URI } from "../constants/endpoints";
|
||||||
import { isNil } from "lodash";
|
import { isNil } from "lodash";
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.min.css";
|
import "react-toastify/dist/ReactToastify.min.css";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
// Type for global state
|
// Type for global state
|
||||||
interface StoreState {
|
interface StoreState {
|
||||||
socketInstances: Record<string, Socket>;
|
socketInstances: Record<string, Socket>;
|
||||||
getSocket: (namespace?: string) => Socket;
|
getSocket: (namespace?: string) => Socket;
|
||||||
disconnectSocket: (namespace: string) => void;
|
disconnectSocket: (namespace: string) => void;
|
||||||
|
queryClientRef: { current: any } | null;
|
||||||
|
setQueryClientRef: (ref: any) => void;
|
||||||
|
|
||||||
comicvine: {
|
comicvine: {
|
||||||
scrapingStatus: string;
|
scrapingStatus: string;
|
||||||
@@ -32,6 +31,8 @@ interface StoreState {
|
|||||||
|
|
||||||
export const useStore = create<StoreState>((set, get) => ({
|
export const useStore = create<StoreState>((set, get) => ({
|
||||||
socketInstances: {},
|
socketInstances: {},
|
||||||
|
queryClientRef: null,
|
||||||
|
setQueryClientRef: (ref: any) => set({ queryClientRef: ref }),
|
||||||
|
|
||||||
getSocket: (namespace = "/") => {
|
getSocket: (namespace = "/") => {
|
||||||
const fullNamespace = namespace === "/" ? "" : namespace;
|
const fullNamespace = namespace === "/" ? "" : namespace;
|
||||||
@@ -97,7 +98,10 @@ export const useStore = create<StoreState>((set, get) => ({
|
|||||||
status: "drained",
|
status: "drained",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
const queryClientRef = get().queryClientRef;
|
||||||
|
if (queryClientRef?.current) {
|
||||||
|
queryClientRef.current.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("CV_SCRAPING_STATUS", (data) => {
|
socket.on("CV_SCRAPING_STATUS", (data) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user