mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: adding dexie
This commit is contained in:
parent
ddd6ff7dbe
commit
f860439fb5
@ -2,6 +2,7 @@ import type { CatalogueEntry } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const getGames = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -14,7 +15,12 @@ const getGames = async (
|
||||
);
|
||||
|
||||
return {
|
||||
results: steamGames,
|
||||
results: steamGames.map((steamGame) => ({
|
||||
title: steamGame.name,
|
||||
shop: "steam",
|
||||
cover: steamUrlBuilder.library(steamGame.id),
|
||||
objectID: steamGame.id,
|
||||
})),
|
||||
cursor: cursor + steamGames.length,
|
||||
};
|
||||
};
|
||||
|
@ -1,39 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadSource } from "@main/entity";
|
||||
import axios from "axios";
|
||||
import { downloadSourceSchema } from "../helpers/validators";
|
||||
import { insertDownloadsFromSource } from "@main/helpers";
|
||||
|
||||
const addDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url: string
|
||||
) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
const source = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const downloadSource = await dataSource.transaction(
|
||||
async (transactionalEntityManager) => {
|
||||
const downloadSource = await transactionalEntityManager
|
||||
.getRepository(DownloadSource)
|
||||
.save({
|
||||
url,
|
||||
name: source.name,
|
||||
downloadCount: source.downloads.length,
|
||||
});
|
||||
|
||||
await insertDownloadsFromSource(
|
||||
transactionalEntityManager,
|
||||
downloadSource,
|
||||
source.downloads
|
||||
);
|
||||
|
||||
return downloadSource;
|
||||
}
|
||||
);
|
||||
|
||||
return downloadSource;
|
||||
};
|
||||
|
||||
registerEvent("addDownloadSource", addDownloadSource);
|
@ -1,10 +0,0 @@
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { RepacksManager } from "@main/services";
|
||||
|
||||
const removeDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
id: number
|
||||
) => downloadSourceRepository.delete(id);
|
||||
|
||||
registerEvent("removeDownloadSource", removeDownloadSource);
|
@ -1,7 +1,13 @@
|
||||
import { downloadSourcesWorker } from "@main/workers";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { fetchDownloadSourcesAndUpdate } from "@main/helpers";
|
||||
import type { DownloadSource } from "@types";
|
||||
|
||||
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
fetchDownloadSourcesAndUpdate();
|
||||
const syncDownloadSources = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
downloadSources: DownloadSource[]
|
||||
) =>
|
||||
downloadSourcesWorker.run(downloadSources, {
|
||||
name: "getUpdatedRepacks",
|
||||
});
|
||||
|
||||
registerEvent("syncDownloadSources", syncDownloadSources);
|
||||
|
@ -1,27 +1,12 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { RepacksManager } from "@main/services";
|
||||
import { downloadSourceWorker } from "@main/workers";
|
||||
import { downloadSourcesWorker } from "@main/workers";
|
||||
|
||||
const validateDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url: string
|
||||
) => {
|
||||
const existingSource = await downloadSourceRepository.findOne({
|
||||
where: { url },
|
||||
) =>
|
||||
downloadSourcesWorker.run(url, {
|
||||
name: "validateDownloadSource",
|
||||
});
|
||||
|
||||
if (existingSource)
|
||||
throw new Error("Source with the same url already exists");
|
||||
|
||||
const repacks = RepacksManager.repacks;
|
||||
|
||||
return downloadSourceWorker.run(
|
||||
{ url, repacks },
|
||||
{
|
||||
name: "validateDownloadSource",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("validateDownloadSource", validateDownloadSource);
|
||||
|
@ -39,8 +39,6 @@ import "./autoupdater/restart-and-install-update";
|
||||
import "./user-preferences/authenticate-real-debrid";
|
||||
import "./download-sources/get-download-sources";
|
||||
import "./download-sources/validate-download-source";
|
||||
import "./download-sources/add-download-source";
|
||||
import "./download-sources/remove-download-source";
|
||||
import "./download-sources/sync-download-sources";
|
||||
import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
|
@ -73,7 +73,6 @@ const getUser = async (
|
||||
recentGames,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
@ -1,76 +0,0 @@
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadSource, Repack } from "@main/entity";
|
||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { RepacksManager } from "@main/services";
|
||||
import { downloadSourceWorker } from "@main/workers";
|
||||
import { chunk } from "lodash-es";
|
||||
import type { EntityManager } from "typeorm";
|
||||
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import { z } from "zod";
|
||||
|
||||
export const insertDownloadsFromSource = async (
|
||||
trx: EntityManager,
|
||||
downloadSource: DownloadSource,
|
||||
downloads: z.infer<typeof downloadSourceSchema>["downloads"]
|
||||
) => {
|
||||
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
||||
(download) => ({
|
||||
title: download.title,
|
||||
uris: JSON.stringify(download.uris),
|
||||
magnet: download.uris[0]!,
|
||||
fileSize: download.fileSize,
|
||||
repacker: downloadSource.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSource: { id: downloadSource.id },
|
||||
})
|
||||
);
|
||||
|
||||
const downloadsChunks = chunk(repacks, 800);
|
||||
|
||||
for (const chunk of downloadsChunks) {
|
||||
await trx
|
||||
.getRepository(Repack)
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(chunk)
|
||||
.updateEntity(false)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDownloadSourcesAndUpdate = async () => {
|
||||
const downloadSources = await downloadSourceRepository.find({
|
||||
order: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const results = await downloadSourceWorker.run(downloadSources, {
|
||||
name: "getUpdatedRepacks",
|
||||
});
|
||||
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
for (const result of results) {
|
||||
if (result.etag !== null) {
|
||||
await transactionalEntityManager.getRepository(DownloadSource).update(
|
||||
{ id: result.id },
|
||||
{
|
||||
etag: result.etag,
|
||||
status: result.status,
|
||||
downloadCount: result.downloads.length,
|
||||
}
|
||||
);
|
||||
|
||||
await insertDownloadsFromSource(
|
||||
transactionalEntityManager,
|
||||
result,
|
||||
result.downloads
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await RepacksManager.updateRepacks();
|
||||
});
|
||||
};
|
@ -36,6 +36,4 @@ export const requestWebPage = async (url: string) => {
|
||||
};
|
||||
|
||||
export const isPortableVersion = () =>
|
||||
process.env.PORTABLE_EXECUTABLE_FILE != null;
|
||||
|
||||
export * from "./download-source";
|
||||
process.env.PORTABLE_EXECUTABLE_FILE !== null;
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
repackRepository,
|
||||
// repackRepository,
|
||||
userPreferencesRepository,
|
||||
} from "./repository";
|
||||
import { UserPreferences } from "./entity";
|
||||
import { RealDebridClient } from "./services/real-debrid";
|
||||
import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
||||
import { publishNewRepacksNotifications } from "./services/notifications";
|
||||
import { MoreThan } from "typeorm";
|
||||
// import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
||||
// import { publishNewRepacksNotifications } from "./services/notifications";
|
||||
// import { MoreThan } from "typeorm";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
|
||||
@ -40,17 +40,17 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
|
||||
startMainLoop();
|
||||
|
||||
const now = new Date();
|
||||
// const now = new Date();
|
||||
|
||||
fetchDownloadSourcesAndUpdate().then(async () => {
|
||||
const newRepacksCount = await repackRepository.count({
|
||||
where: {
|
||||
createdAt: MoreThan(now),
|
||||
},
|
||||
});
|
||||
// fetchDownloadSourcesAndUpdate().then(async () => {
|
||||
// const newRepacksCount = await repackRepository.count({
|
||||
// where: {
|
||||
// createdAt: MoreThan(now),
|
||||
// },
|
||||
// });
|
||||
|
||||
if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
|
||||
});
|
||||
// if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
|
||||
// });
|
||||
};
|
||||
|
||||
userPreferencesRepository
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import type { DownloadSource, GameRepack } from "@types";
|
||||
import type { DownloadSource } from "@types";
|
||||
import axios, { AxiosError, AxiosHeaders } from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -49,23 +49,11 @@ export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
|
||||
return results;
|
||||
};
|
||||
|
||||
export const validateDownloadSource = async ({
|
||||
url,
|
||||
repacks,
|
||||
}: {
|
||||
url: string;
|
||||
repacks: GameRepack[];
|
||||
}) => {
|
||||
export const validateDownloadSource = async (url: string) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
const source = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const existingUris = source.downloads
|
||||
.flatMap((download) => download.uris)
|
||||
.filter((uri) => repacks.some((repack) => repack.magnet === uri));
|
||||
|
||||
return {
|
||||
name: source.name,
|
||||
downloadCount: source.downloads.length - existingUris.length,
|
||||
...downloadSourceSchema.parse(response.data),
|
||||
etag: response.headers["etag"],
|
||||
};
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
||||
import downloadSourceWorkerPath from "./download-source.worker?modulePath";
|
||||
import downloadSourcesWorkerPath from "./download-sources.worker?modulePath";
|
||||
|
||||
import Piscina from "piscina";
|
||||
|
||||
@ -14,6 +14,6 @@ export const steamGamesWorker = new Piscina({
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
export const downloadSourceWorker = new Piscina({
|
||||
filename: downloadSourceWorkerPath,
|
||||
export const downloadSourcesWorker = new Piscina({
|
||||
filename: downloadSourcesWorkerPath,
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
GameRunning,
|
||||
FriendRequestAction,
|
||||
UpdateProfileRequest,
|
||||
DownloadSource,
|
||||
} from "@types";
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
|
||||
@ -64,11 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||
validateDownloadSource: (url: string) =>
|
||||
ipcRenderer.invoke("validateDownloadSource", url),
|
||||
addDownloadSource: (url: string) =>
|
||||
ipcRenderer.invoke("addDownloadSource", url),
|
||||
removeDownloadSource: (id: number) =>
|
||||
ipcRenderer.invoke("removeDownloadSource", id),
|
||||
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
||||
syncDownloadSources: (downloadSources: DownloadSource[]) =>
|
||||
ipcRenderer.invoke("syncDownloadSources", downloadSources),
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useCallback, useContext, useEffect, useRef } from "react";
|
||||
|
||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||
|
||||
@ -26,10 +26,8 @@ import {
|
||||
} from "@renderer/features";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||
import { RepacksContextProvider } from "./context";
|
||||
import { downloadSourcesWorker } from "./workers";
|
||||
|
||||
downloadSourcesWorker.postMessage("OK");
|
||||
import { migrationWorker } from "./workers";
|
||||
import { repacksContext } from "./context";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
@ -43,6 +41,8 @@ export function App() {
|
||||
|
||||
const { clearDownload, setLastPacket } = useDownload();
|
||||
|
||||
const { indexRepacks } = useContext(repacksContext);
|
||||
|
||||
const {
|
||||
isFriendsModalVisible,
|
||||
friendRequetsModalTab,
|
||||
@ -210,53 +210,70 @@ export function App() {
|
||||
});
|
||||
}, [dispatch, draggingDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
// window.electron.getRepacks().then((repacks) => {
|
||||
// migrationWorker.postMessage(["MIGRATE_REPACKS", repacks]);
|
||||
// });
|
||||
// window.electron.getDownloadSources().then((downloadSources) => {
|
||||
// migrationWorker.postMessage([
|
||||
// "MIGRATE_DOWNLOAD_SOURCES",
|
||||
// downloadSources,
|
||||
// ]);
|
||||
// });
|
||||
// migrationWorker.onmessage = (
|
||||
// event: MessageEvent<"MIGRATE_REPACKS_COMPLETE">
|
||||
// ) => {
|
||||
// if (event.data === "MIGRATE_REPACKS_COMPLETE") {
|
||||
// indexRepacks();
|
||||
// }
|
||||
// };
|
||||
}, [indexRepacks]);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
dispatch(closeToast());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<RepacksContextProvider>
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
<div className={styles.titleBar}>
|
||||
<h4>Hydra</h4>
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
<div className={styles.titleBar}>
|
||||
<h4>Hydra</h4>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
/>
|
||||
|
||||
{userDetails && (
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
initialTab={friendRequetsModalTab}
|
||||
onClose={hideFriendsModal}
|
||||
userId={friendModalUserId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{userDetails && (
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
initialTab={friendRequetsModalTab}
|
||||
onClose={hideFriendsModal}
|
||||
userId={friendModalUserId}
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
||||
<article className={styles.container}>
|
||||
<Header
|
||||
onSearch={handleSearch}
|
||||
search={search}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<Sidebar />
|
||||
<section ref={contentRef} className={styles.content}>
|
||||
<Outlet />
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<article className={styles.container}>
|
||||
<Header
|
||||
onSearch={handleSearch}
|
||||
search={search}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
|
||||
<section ref={contentRef} className={styles.content}>
|
||||
<Outlet />
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<BottomPanel />
|
||||
</>
|
||||
</RepacksContextProvider>
|
||||
<BottomPanel />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ import { repacksWorker } from "@renderer/workers";
|
||||
|
||||
export interface RepacksContext {
|
||||
searchRepacks: (query: string) => Promise<GameRepack[]>;
|
||||
indexRepacks: () => void;
|
||||
isIndexingRepacks: boolean;
|
||||
}
|
||||
|
||||
export const repacksContext = createContext<RepacksContext>({
|
||||
searchRepacks: async () => [] as GameRepack[],
|
||||
indexRepacks: () => {},
|
||||
isIndexingRepacks: false,
|
||||
});
|
||||
|
||||
@ -37,7 +39,8 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const indexRepacks = useCallback(() => {
|
||||
setIsIndexingRepacks(true);
|
||||
repacksWorker.postMessage("INDEX_REPACKS");
|
||||
|
||||
repacksWorker.onmessage = () => {
|
||||
@ -45,10 +48,15 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
indexRepacks();
|
||||
}, [indexRepacks]);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
searchRepacks,
|
||||
indexRepacks,
|
||||
isIndexingRepacks,
|
||||
}}
|
||||
>
|
||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -25,6 +25,7 @@ import type {
|
||||
UserStats,
|
||||
UserDetails,
|
||||
FriendRequestSync,
|
||||
DownloadSourceValidationResult,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@ -106,10 +107,8 @@ declare global {
|
||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||
validateDownloadSource: (
|
||||
url: string
|
||||
) => Promise<{ name: string; downloadCount: number }>;
|
||||
addDownloadSource: (url: string) => Promise<DownloadSource>;
|
||||
removeDownloadSource: (id: number) => Promise<void>;
|
||||
syncDownloadSources: () => Promise<void>;
|
||||
) => Promise<DownloadSourceValidationResult>;
|
||||
syncDownloadSources: (downloadSources: DownloadSource[]) => Promise<void>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
|
@ -3,7 +3,7 @@ import { Dexie } from "dexie";
|
||||
export const db = new Dexie("Hydra");
|
||||
|
||||
db.version(1).stores({
|
||||
repacks: `++id, title, uri, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
||||
});
|
||||
|
||||
|
@ -30,6 +30,7 @@ import { store } from "./store";
|
||||
import resources from "@locales";
|
||||
|
||||
import "./workers";
|
||||
import { RepacksContextProvider } from "./context";
|
||||
|
||||
Sentry.init({});
|
||||
|
||||
@ -56,19 +57,21 @@ i18n
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route element={<App />}>
|
||||
<Route path="/" Component={Home} />
|
||||
<Route path="/catalogue" Component={Catalogue} />
|
||||
<Route path="/downloads" Component={Downloads} />
|
||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
<RepacksContextProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route element={<App />}>
|
||||
<Route path="/" Component={Home} />
|
||||
<Route path="/catalogue" Component={Catalogue} />
|
||||
<Route path="/downloads" Component={Downloads} />
|
||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</RepacksContextProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
@ -9,6 +9,8 @@ import { useForm } from "react-hook-form";
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { downloadSourcesTable } from "@renderer/dexie";
|
||||
import { DownloadSourceValidationResult } from "@types";
|
||||
import { downloadSourcesWorker } from "@renderer/workers";
|
||||
|
||||
interface AddDownloadSourceModalProps {
|
||||
visible: boolean;
|
||||
@ -40,41 +42,35 @@ export function AddDownloadSourceModal({
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormValues>({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const [validationResult, setValidationResult] = useState<{
|
||||
name: string;
|
||||
downloadCount: number;
|
||||
} | null>(null);
|
||||
const [validationResult, setValidationResult] =
|
||||
useState<DownloadSourceValidationResult | null>(null);
|
||||
|
||||
const { sourceUrl } = useContext(settingsContext);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormValues) => {
|
||||
setIsLoading(true);
|
||||
const existingDownloadSource = await downloadSourcesTable
|
||||
.where({ url: values.url })
|
||||
.first();
|
||||
|
||||
try {
|
||||
const result = await window.electron.validateDownloadSource(values.url);
|
||||
setValidationResult(result);
|
||||
if (existingDownloadSource) {
|
||||
setError("url", {
|
||||
type: "server",
|
||||
message: t("source_already_exists"),
|
||||
});
|
||||
|
||||
setUrl(values.url);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (
|
||||
error.message.endsWith("Source with the same url already exists")
|
||||
) {
|
||||
setError("url", {
|
||||
type: "server",
|
||||
message: t("source_already_exists"),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await window.electron.validateDownloadSource(values.url);
|
||||
setValidationResult(result);
|
||||
|
||||
setUrl(values.url);
|
||||
},
|
||||
[setError, t]
|
||||
);
|
||||
@ -92,12 +88,23 @@ export function AddDownloadSourceModal({
|
||||
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
|
||||
|
||||
const handleAddDownloadSource = async () => {
|
||||
await downloadSourcesTable.add({
|
||||
url,
|
||||
});
|
||||
await window.electron.addDownloadSource(url);
|
||||
onClose();
|
||||
onAddDownloadSource();
|
||||
setIsLoading(true);
|
||||
|
||||
if (validationResult) {
|
||||
const channel = new BroadcastChannel(`download_sources:import:${url}`);
|
||||
|
||||
downloadSourcesWorker.postMessage([
|
||||
"IMPORT_DOWNLOAD_SOURCE",
|
||||
{ ...validationResult, url },
|
||||
]);
|
||||
|
||||
channel.onmessage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
onClose();
|
||||
onAddDownloadSource();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -126,7 +133,7 @@ export function AddDownloadSourceModal({
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={isLoading}
|
||||
disabled={isSubmitting || isLoading}
|
||||
>
|
||||
{t("validate_download_source")}
|
||||
</Button>
|
||||
@ -152,9 +159,9 @@ export function AddDownloadSourceModal({
|
||||
<h4>{validationResult?.name}</h4>
|
||||
<small>
|
||||
{t("found_download_option", {
|
||||
count: validationResult?.downloadCount,
|
||||
count: validationResult?.downloads.length,
|
||||
countFormatted:
|
||||
validationResult?.downloadCount.toLocaleString(),
|
||||
validationResult?.downloads.length.toLocaleString(),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
|
@ -10,8 +10,9 @@ import { AddDownloadSourceModal } from "./add-download-source-modal";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
import { repacksContext, settingsContext } from "@renderer/context";
|
||||
import { downloadSourcesTable } from "@renderer/dexie";
|
||||
import { downloadSourcesWorker } from "@renderer/workers";
|
||||
|
||||
export function SettingsDownloadSources() {
|
||||
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||
@ -19,16 +20,23 @@ export function SettingsDownloadSources() {
|
||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||
const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
|
||||
useState(false);
|
||||
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
|
||||
useState(false);
|
||||
|
||||
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const { indexRepacks } = useContext(repacksContext);
|
||||
|
||||
const getDownloadSources = async () => {
|
||||
downloadSourcesTable.toArray().then((sources) => {
|
||||
setDownloadSources(sources);
|
||||
});
|
||||
await downloadSourcesTable
|
||||
.toCollection()
|
||||
.sortBy("createdAt")
|
||||
.then((sources) => {
|
||||
setDownloadSources(sources.reverse());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -39,18 +47,23 @@ export function SettingsDownloadSources() {
|
||||
if (sourceUrl) setShowAddDownloadSourceModal(true);
|
||||
}, [sourceUrl]);
|
||||
|
||||
const handleRemoveSource = async (id: number) => {
|
||||
await db.transaction("rw", downloadSourcesTable, repacksTable, async () => {
|
||||
await downloadSourcesTable.where({ id }).delete();
|
||||
await repacksTable.where({ downloadSourceId: id }).delete();
|
||||
});
|
||||
const handleRemoveSource = (id: number) => {
|
||||
setIsRemovingDownloadSource(true);
|
||||
const channel = new BroadcastChannel(`download_sources:delete:${id}`);
|
||||
|
||||
showSuccessToast(t("removed_download_source"));
|
||||
downloadSourcesWorker.postMessage(["DELETE_DOWNLOAD_SOURCE", id]);
|
||||
|
||||
getDownloadSources();
|
||||
channel.onmessage = () => {
|
||||
showSuccessToast(t("removed_download_source"));
|
||||
|
||||
getDownloadSources();
|
||||
indexRepacks();
|
||||
setIsRemovingDownloadSource(false);
|
||||
};
|
||||
};
|
||||
|
||||
const handleAddDownloadSource = async () => {
|
||||
indexRepacks();
|
||||
await getDownloadSources();
|
||||
showSuccessToast(t("added_download_source"));
|
||||
};
|
||||
@ -59,7 +72,7 @@ export function SettingsDownloadSources() {
|
||||
setIsSyncingDownloadSources(true);
|
||||
|
||||
window.electron
|
||||
.syncDownloadSources()
|
||||
.syncDownloadSources(downloadSources)
|
||||
.then(() => {
|
||||
showSuccessToast(t("download_sources_synced"));
|
||||
getDownloadSources();
|
||||
@ -93,7 +106,11 @@ export function SettingsDownloadSources() {
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
disabled={!downloadSources.length || isSyncingDownloadSources}
|
||||
disabled={
|
||||
!downloadSources.length ||
|
||||
isSyncingDownloadSources ||
|
||||
isRemovingDownloadSource
|
||||
}
|
||||
onClick={syncDownloadSources}
|
||||
>
|
||||
<SyncIcon />
|
||||
@ -104,6 +121,7 @@ export function SettingsDownloadSources() {
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||
disabled={isSyncingDownloadSources}
|
||||
>
|
||||
<PlusCircleIcon />
|
||||
{t("add_download_source")}
|
||||
@ -153,6 +171,7 @@ export function SettingsDownloadSources() {
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||
disabled={isRemovingDownloadSource}
|
||||
>
|
||||
<NoEntryIcon />
|
||||
{t("remove_download_source")}
|
||||
|
@ -1,8 +1,63 @@
|
||||
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import type { DownloadSourceValidationResult } from "@types";
|
||||
|
||||
self.onmessage = () => {
|
||||
db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||
await repacksTable.where({ downloadSourceId: 10 }).delete();
|
||||
await downloadSourcesTable.where({ id: 10 }).delete();
|
||||
});
|
||||
type Payload =
|
||||
| ["IMPORT_DOWNLOAD_SOURCE", DownloadSourceValidationResult & { url: string }]
|
||||
| ["DELETE_DOWNLOAD_SOURCE", number];
|
||||
|
||||
db.open();
|
||||
|
||||
self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||
const [type, data] = event.data;
|
||||
|
||||
if (type === "DELETE_DOWNLOAD_SOURCE") {
|
||||
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||
await repacksTable.where({ downloadSourceId: data }).delete();
|
||||
await downloadSourcesTable.where({ id: data }).delete();
|
||||
});
|
||||
|
||||
const channel = new BroadcastChannel(`download_sources:delete:${data}`);
|
||||
|
||||
channel.postMessage(true);
|
||||
}
|
||||
|
||||
if (type === "IMPORT_DOWNLOAD_SOURCE") {
|
||||
const result = data;
|
||||
|
||||
await db.transaction("rw", downloadSourcesTable, repacksTable, async () => {
|
||||
const now = new Date();
|
||||
|
||||
const id = await downloadSourcesTable.add({
|
||||
url: result.url,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
status: DownloadSourceStatus.UpToDate,
|
||||
downloadCount: result.downloads.length,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const downloadSource = await downloadSourcesTable.get(id);
|
||||
|
||||
const repacks = result.downloads.map((download) => ({
|
||||
title: download.title,
|
||||
uris: download.uris,
|
||||
fileSize: download.fileSize,
|
||||
repacker: result.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSourceId: downloadSource!.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
|
||||
await repacksTable.bulkAdd(repacks);
|
||||
});
|
||||
|
||||
const channel = new BroadcastChannel(
|
||||
`download_sources:import:${result.url}`
|
||||
);
|
||||
|
||||
channel.postMessage(true);
|
||||
}
|
||||
};
|
||||
|
@ -2,23 +2,6 @@ import MigrationWorker from "./migration.worker?worker";
|
||||
import RepacksWorker from "./repacks.worker?worker";
|
||||
import DownloadSourcesWorker from "./download-sources.worker?worker";
|
||||
|
||||
// const migrationWorker = new MigrationWorker();
|
||||
export const migrationWorker = new MigrationWorker();
|
||||
export const repacksWorker = new RepacksWorker();
|
||||
export const downloadSourcesWorker = new DownloadSourcesWorker();
|
||||
|
||||
// window.electron.getRepacks().then((repacks) => {
|
||||
// console.log(repacks);
|
||||
// migrationWorker.postMessage(["MIGRATE_REPACKS", repacks]);
|
||||
// });
|
||||
|
||||
// window.electron.getDownloadSources().then((downloadSources) => {
|
||||
// migrationWorker.postMessage(["MIGRATE_DOWNLOAD_SOURCES", downloadSources]);
|
||||
// });
|
||||
|
||||
// migrationWorker.onmessage = (event) => {
|
||||
// console.log(event.data);
|
||||
// };
|
||||
|
||||
// setTimeout(() => {
|
||||
// repacksWorker.postMessage("god");
|
||||
// }, 500);
|
||||
|
@ -1,32 +1,43 @@
|
||||
import { downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
import { DownloadSource, GameRepack } from "@types";
|
||||
|
||||
export type Payload =
|
||||
| ["MIGRATE_REPACKS", GameRepack[]]
|
||||
| ["MIGRATE_DOWNLOAD_SOURCES", DownloadSource[]];
|
||||
export type Payload = [DownloadSource[], GameRepack[]];
|
||||
|
||||
self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||
const [type, data] = event.data;
|
||||
const [downloadSources, gameRepacks] = event.data;
|
||||
|
||||
if (type === "MIGRATE_DOWNLOAD_SOURCES") {
|
||||
const dexieDownloadSources = await downloadSourcesTable.count();
|
||||
const downloadSourcesCount = await downloadSourcesTable.count();
|
||||
|
||||
if (data.length !== dexieDownloadSources) {
|
||||
await downloadSourcesTable.clear();
|
||||
await downloadSourcesTable.bulkAdd(data);
|
||||
}
|
||||
|
||||
self.postMessage("MIGRATE_DOWNLOAD_SOURCES_COMPLETE");
|
||||
if (downloadSources.length > downloadSourcesCount) {
|
||||
await db.transaction(
|
||||
"rw",
|
||||
downloadSourcesTable,
|
||||
repacksTable,
|
||||
async () => {}
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "MIGRATE_REPACKS") {
|
||||
const dexieRepacks = await repacksTable.count();
|
||||
// if (type === "MIGRATE_DOWNLOAD_SOURCES") {
|
||||
// const dexieDownloadSources = await downloadSourcesTable.count();
|
||||
|
||||
if (data.length !== dexieRepacks) {
|
||||
await repacksTable.clear();
|
||||
await repacksTable.bulkAdd(data);
|
||||
}
|
||||
// if (data.length > dexieDownloadSources) {
|
||||
// await downloadSourcesTable.clear();
|
||||
// await downloadSourcesTable.bulkAdd(data);
|
||||
// }
|
||||
|
||||
self.postMessage("MIGRATE_REPACKS_COMPLETE");
|
||||
}
|
||||
// self.postMessage("MIGRATE_DOWNLOAD_SOURCES_COMPLETE");
|
||||
// }
|
||||
|
||||
// if (type === "MIGRATE_REPACKS") {
|
||||
// const dexieRepacks = await repacksTable.count();
|
||||
|
||||
// if (data.length > dexieRepacks) {
|
||||
// await repacksTable.clear();
|
||||
// await repacksTable.bulkAdd(
|
||||
// data.map((repack) => ({ ...repack, uris: JSON.stringify(repack.uris) }))
|
||||
// );
|
||||
// }
|
||||
|
||||
// self.postMessage("MIGRATE_REPACKS_COMPLETE");
|
||||
// }
|
||||
};
|
||||
|
@ -37,11 +37,9 @@ self.onmessage = async (
|
||||
const results = index.search(formatName(query)).map((index) => {
|
||||
const repack = state.repacks.at(index as number) as SerializedGameRepack;
|
||||
|
||||
const uris = JSON.parse(repack.uris);
|
||||
|
||||
return {
|
||||
...repack,
|
||||
uris: [...uris, repack.magnet].filter(Boolean),
|
||||
uris: [...repack.uris, repack.magnet].filter(Boolean),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -224,6 +224,19 @@ export interface UpdateProfileRequest {
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export interface DownloadSourceDownload {
|
||||
title: string;
|
||||
uris: string[];
|
||||
uploadDate: string;
|
||||
fileSize: string;
|
||||
}
|
||||
|
||||
export interface DownloadSourceValidationResult {
|
||||
name: string;
|
||||
downloads: DownloadSourceDownload[];
|
||||
etag: string;
|
||||
}
|
||||
|
||||
export interface DownloadSource {
|
||||
id: number;
|
||||
name: string;
|
||||
|
Loading…
Reference in New Issue
Block a user