feat: adding dexie

This commit is contained in:
Chubby Granny Chaser 2024-09-22 17:43:05 +01:00
parent ddd6ff7dbe
commit f860439fb5
No known key found for this signature in database
25 changed files with 311 additions and 345 deletions

View File

@ -2,6 +2,7 @@ import type { CatalogueEntry } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
import { steamUrlBuilder } from "@shared";
const getGames = async ( const getGames = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -14,7 +15,12 @@ const getGames = async (
); );
return { return {
results: steamGames, results: steamGames.map((steamGame) => ({
title: steamGame.name,
shop: "steam",
cover: steamUrlBuilder.library(steamGame.id),
objectID: steamGame.id,
})),
cursor: cursor + steamGames.length, cursor: cursor + steamGames.length,
}; };
}; };

View File

@ -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);

View File

@ -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);

View File

@ -1,7 +1,13 @@
import { downloadSourcesWorker } from "@main/workers";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { fetchDownloadSourcesAndUpdate } from "@main/helpers"; import type { DownloadSource } from "@types";
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => const syncDownloadSources = async (
fetchDownloadSourcesAndUpdate(); _event: Electron.IpcMainInvokeEvent,
downloadSources: DownloadSource[]
) =>
downloadSourcesWorker.run(downloadSources, {
name: "getUpdatedRepacks",
});
registerEvent("syncDownloadSources", syncDownloadSources); registerEvent("syncDownloadSources", syncDownloadSources);

View File

@ -1,27 +1,12 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { downloadSourceRepository } from "@main/repository"; import { downloadSourcesWorker } from "@main/workers";
import { RepacksManager } from "@main/services";
import { downloadSourceWorker } from "@main/workers";
const validateDownloadSource = async ( const validateDownloadSource = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
url: string url: string
) => { ) =>
const existingSource = await downloadSourceRepository.findOne({ downloadSourcesWorker.run(url, {
where: { 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); registerEvent("validateDownloadSource", validateDownloadSource);

View File

@ -39,8 +39,6 @@ import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid"; import "./user-preferences/authenticate-real-debrid";
import "./download-sources/get-download-sources"; import "./download-sources/get-download-sources";
import "./download-sources/validate-download-source"; 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 "./download-sources/sync-download-sources";
import "./auth/sign-out"; import "./auth/sign-out";
import "./auth/open-auth-window"; import "./auth/open-auth-window";

View File

@ -73,7 +73,6 @@ const getUser = async (
recentGames, recentGames,
}; };
} catch (err) { } catch (err) {
console.log(err);
return null; return null;
} }
}; };

View File

@ -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();
});
};

View File

@ -36,6 +36,4 @@ export const requestWebPage = async (url: string) => {
}; };
export const isPortableVersion = () => export const isPortableVersion = () =>
process.env.PORTABLE_EXECUTABLE_FILE != null; process.env.PORTABLE_EXECUTABLE_FILE !== null;
export * from "./download-source";

View File

@ -1,14 +1,14 @@
import { DownloadManager, PythonInstance, startMainLoop } from "./services"; import { DownloadManager, PythonInstance, startMainLoop } from "./services";
import { import {
downloadQueueRepository, downloadQueueRepository,
repackRepository, // repackRepository,
userPreferencesRepository, userPreferencesRepository,
} from "./repository"; } from "./repository";
import { UserPreferences } from "./entity"; import { UserPreferences } from "./entity";
import { RealDebridClient } from "./services/real-debrid"; import { RealDebridClient } from "./services/real-debrid";
import { fetchDownloadSourcesAndUpdate } from "./helpers"; // import { fetchDownloadSourcesAndUpdate } from "./helpers";
import { publishNewRepacksNotifications } from "./services/notifications"; // import { publishNewRepacksNotifications } from "./services/notifications";
import { MoreThan } from "typeorm"; // import { MoreThan } from "typeorm";
import { HydraApi } from "./services/hydra-api"; import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync"; import { uploadGamesBatch } from "./services/library-sync";
@ -40,17 +40,17 @@ const loadState = async (userPreferences: UserPreferences | null) => {
startMainLoop(); startMainLoop();
const now = new Date(); // const now = new Date();
fetchDownloadSourcesAndUpdate().then(async () => { // fetchDownloadSourcesAndUpdate().then(async () => {
const newRepacksCount = await repackRepository.count({ // const newRepacksCount = await repackRepository.count({
where: { // where: {
createdAt: MoreThan(now), // createdAt: MoreThan(now),
}, // },
}); // });
if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount); // if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
}); // });
}; };
userPreferencesRepository userPreferencesRepository

View File

@ -1,6 +1,6 @@
import { downloadSourceSchema } from "@main/events/helpers/validators"; import { downloadSourceSchema } from "@main/events/helpers/validators";
import { DownloadSourceStatus } from "@shared"; import { DownloadSourceStatus } from "@shared";
import type { DownloadSource, GameRepack } from "@types"; import type { DownloadSource } from "@types";
import axios, { AxiosError, AxiosHeaders } from "axios"; import axios, { AxiosError, AxiosHeaders } from "axios";
import { z } from "zod"; import { z } from "zod";
@ -49,23 +49,11 @@ export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
return results; return results;
}; };
export const validateDownloadSource = async ({ export const validateDownloadSource = async (url: string) => {
url,
repacks,
}: {
url: string;
repacks: GameRepack[];
}) => {
const response = await axios.get(url); 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 { return {
name: source.name, ...downloadSourceSchema.parse(response.data),
downloadCount: source.downloads.length - existingUris.length, etag: response.headers["etag"],
}; };
}; };

View File

@ -1,6 +1,6 @@
import path from "node:path"; import path from "node:path";
import steamGamesWorkerPath from "./steam-games.worker?modulePath"; 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"; import Piscina from "piscina";
@ -14,6 +14,6 @@ export const steamGamesWorker = new Piscina({
maxThreads: 1, maxThreads: 1,
}); });
export const downloadSourceWorker = new Piscina({ export const downloadSourcesWorker = new Piscina({
filename: downloadSourceWorkerPath, filename: downloadSourcesWorkerPath,
}); });

View File

@ -11,6 +11,7 @@ import type {
GameRunning, GameRunning,
FriendRequestAction, FriendRequestAction,
UpdateProfileRequest, UpdateProfileRequest,
DownloadSource,
} from "@types"; } from "@types";
import type { CatalogueCategory } from "@shared"; import type { CatalogueCategory } from "@shared";
@ -64,11 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"), getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
validateDownloadSource: (url: string) => validateDownloadSource: (url: string) =>
ipcRenderer.invoke("validateDownloadSource", url), ipcRenderer.invoke("validateDownloadSource", url),
addDownloadSource: (url: string) => syncDownloadSources: (downloadSources: DownloadSource[]) =>
ipcRenderer.invoke("addDownloadSource", url), ipcRenderer.invoke("syncDownloadSources", downloadSources),
removeDownloadSource: (id: number) =>
ipcRenderer.invoke("removeDownloadSource", id),
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
/* Library */ /* Library */
addGameToLibrary: (objectID: string, title: string, shop: GameShop) => addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>

View File

@ -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"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
@ -26,10 +26,8 @@ import {
} from "@renderer/features"; } from "@renderer/features";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
import { RepacksContextProvider } from "./context"; import { migrationWorker } from "./workers";
import { downloadSourcesWorker } from "./workers"; import { repacksContext } from "./context";
downloadSourcesWorker.postMessage("OK");
export interface AppProps { export interface AppProps {
children: React.ReactNode; children: React.ReactNode;
@ -43,6 +41,8 @@ export function App() {
const { clearDownload, setLastPacket } = useDownload(); const { clearDownload, setLastPacket } = useDownload();
const { indexRepacks } = useContext(repacksContext);
const { const {
isFriendsModalVisible, isFriendsModalVisible,
friendRequetsModalTab, friendRequetsModalTab,
@ -210,53 +210,70 @@ export function App() {
}); });
}, [dispatch, draggingDisabled]); }, [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(() => { const handleToastClose = useCallback(() => {
dispatch(closeToast()); dispatch(closeToast());
}, [dispatch]); }, [dispatch]);
return ( return (
<RepacksContextProvider> <>
<> {window.electron.platform === "win32" && (
{window.electron.platform === "win32" && ( <div className={styles.titleBar}>
<div className={styles.titleBar}> <h4>Hydra</h4>
<h4>Hydra</h4> </div>
</div> )}
)}
<Toast <Toast
visible={toast.visible} visible={toast.visible}
message={toast.message} message={toast.message}
type={toast.type} type={toast.type}
onClose={handleToastClose} onClose={handleToastClose}
/>
{userDetails && (
<UserFriendModal
visible={isFriendsModalVisible}
initialTab={friendRequetsModalTab}
onClose={hideFriendsModal}
userId={friendModalUserId}
/> />
)}
{userDetails && ( <main>
<UserFriendModal <Sidebar />
visible={isFriendsModalVisible}
initialTab={friendRequetsModalTab} <article className={styles.container}>
onClose={hideFriendsModal} <Header
userId={friendModalUserId} onSearch={handleSearch}
search={search}
onClear={handleClear}
/> />
)}
<main> <section ref={contentRef} className={styles.content}>
<Sidebar /> <Outlet />
</section>
</article>
</main>
<article className={styles.container}> <BottomPanel />
<Header </>
onSearch={handleSearch}
search={search}
onClear={handleClear}
/>
<section ref={contentRef} className={styles.content}>
<Outlet />
</section>
</article>
</main>
<BottomPanel />
</>
</RepacksContextProvider>
); );
} }

View File

@ -5,11 +5,13 @@ import { repacksWorker } from "@renderer/workers";
export interface RepacksContext { export interface RepacksContext {
searchRepacks: (query: string) => Promise<GameRepack[]>; searchRepacks: (query: string) => Promise<GameRepack[]>;
indexRepacks: () => void;
isIndexingRepacks: boolean; isIndexingRepacks: boolean;
} }
export const repacksContext = createContext<RepacksContext>({ export const repacksContext = createContext<RepacksContext>({
searchRepacks: async () => [] as GameRepack[], searchRepacks: async () => [] as GameRepack[],
indexRepacks: () => {},
isIndexingRepacks: false, isIndexingRepacks: false,
}); });
@ -37,7 +39,8 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
}); });
}, []); }, []);
useEffect(() => { const indexRepacks = useCallback(() => {
setIsIndexingRepacks(true);
repacksWorker.postMessage("INDEX_REPACKS"); repacksWorker.postMessage("INDEX_REPACKS");
repacksWorker.onmessage = () => { repacksWorker.onmessage = () => {
@ -45,10 +48,15 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
}; };
}, []); }, []);
useEffect(() => {
indexRepacks();
}, [indexRepacks]);
return ( return (
<Provider <Provider
value={{ value={{
searchRepacks, searchRepacks,
indexRepacks,
isIndexingRepacks, isIndexingRepacks,
}} }}
> >

View File

@ -25,6 +25,7 @@ import type {
UserStats, UserStats,
UserDetails, UserDetails,
FriendRequestSync, FriendRequestSync,
DownloadSourceValidationResult,
} from "@types"; } from "@types";
import type { DiskSpace } from "check-disk-space"; import type { DiskSpace } from "check-disk-space";
@ -106,10 +107,8 @@ declare global {
getDownloadSources: () => Promise<DownloadSource[]>; getDownloadSources: () => Promise<DownloadSource[]>;
validateDownloadSource: ( validateDownloadSource: (
url: string url: string
) => Promise<{ name: string; downloadCount: number }>; ) => Promise<DownloadSourceValidationResult>;
addDownloadSource: (url: string) => Promise<DownloadSource>; syncDownloadSources: (downloadSources: DownloadSource[]) => Promise<void>;
removeDownloadSource: (id: number) => Promise<void>;
syncDownloadSources: () => Promise<void>;
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>; getDiskFreeSpace: (path: string) => Promise<DiskSpace>;

View File

@ -3,7 +3,7 @@ import { Dexie } from "dexie";
export const db = new Dexie("Hydra"); export const db = new Dexie("Hydra");
db.version(1).stores({ 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`, downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
}); });

View File

@ -30,6 +30,7 @@ import { store } from "./store";
import resources from "@locales"; import resources from "@locales";
import "./workers"; import "./workers";
import { RepacksContextProvider } from "./context";
Sentry.init({}); Sentry.init({});
@ -56,19 +57,21 @@ i18n
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<HashRouter> <RepacksContextProvider>
<Routes> <HashRouter>
<Route element={<App />}> <Routes>
<Route path="/" Component={Home} /> <Route element={<App />}>
<Route path="/catalogue" Component={Catalogue} /> <Route path="/" Component={Home} />
<Route path="/downloads" Component={Downloads} /> <Route path="/catalogue" Component={Catalogue} />
<Route path="/game/:shop/:objectID" Component={GameDetails} /> <Route path="/downloads" Component={Downloads} />
<Route path="/search" Component={SearchResults} /> <Route path="/game/:shop/:objectID" Component={GameDetails} />
<Route path="/settings" Component={Settings} /> <Route path="/search" Component={SearchResults} />
<Route path="/profile/:userId" Component={Profile} /> <Route path="/settings" Component={Settings} />
</Route> <Route path="/profile/:userId" Component={Profile} />
</Routes> </Route>
</HashRouter> </Routes>
</HashRouter>
</RepacksContextProvider>
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -9,6 +9,8 @@ import { useForm } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { downloadSourcesTable } from "@renderer/dexie"; import { downloadSourcesTable } from "@renderer/dexie";
import { DownloadSourceValidationResult } from "@types";
import { downloadSourcesWorker } from "@renderer/workers";
interface AddDownloadSourceModalProps { interface AddDownloadSourceModalProps {
visible: boolean; visible: boolean;
@ -40,41 +42,35 @@ export function AddDownloadSourceModal({
setValue, setValue,
setError, setError,
clearErrors, clearErrors,
formState: { errors }, formState: { errors, isSubmitting },
} = useForm<FormValues>({ } = useForm<FormValues>({
resolver: yupResolver(schema), resolver: yupResolver(schema),
}); });
const [validationResult, setValidationResult] = useState<{ const [validationResult, setValidationResult] =
name: string; useState<DownloadSourceValidationResult | null>(null);
downloadCount: number;
} | null>(null);
const { sourceUrl } = useContext(settingsContext); const { sourceUrl } = useContext(settingsContext);
const onSubmit = useCallback( const onSubmit = useCallback(
async (values: FormValues) => { async (values: FormValues) => {
setIsLoading(true); const existingDownloadSource = await downloadSourcesTable
.where({ url: values.url })
.first();
try { if (existingDownloadSource) {
const result = await window.electron.validateDownloadSource(values.url); setError("url", {
setValidationResult(result); type: "server",
message: t("source_already_exists"),
});
setUrl(values.url); return;
} 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);
} }
const result = await window.electron.validateDownloadSource(values.url);
setValidationResult(result);
setUrl(values.url);
}, },
[setError, t] [setError, t]
); );
@ -92,12 +88,23 @@ export function AddDownloadSourceModal({
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]); }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
const handleAddDownloadSource = async () => { const handleAddDownloadSource = async () => {
await downloadSourcesTable.add({ setIsLoading(true);
url,
}); if (validationResult) {
await window.electron.addDownloadSource(url); const channel = new BroadcastChannel(`download_sources:import:${url}`);
onClose();
onAddDownloadSource(); downloadSourcesWorker.postMessage([
"IMPORT_DOWNLOAD_SOURCE",
{ ...validationResult, url },
]);
channel.onmessage = () => {
setIsLoading(false);
onClose();
onAddDownloadSource();
};
}
}; };
return ( return (
@ -126,7 +133,7 @@ export function AddDownloadSourceModal({
theme="outline" theme="outline"
style={{ alignSelf: "flex-end" }} style={{ alignSelf: "flex-end" }}
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}
disabled={isLoading} disabled={isSubmitting || isLoading}
> >
{t("validate_download_source")} {t("validate_download_source")}
</Button> </Button>
@ -152,9 +159,9 @@ export function AddDownloadSourceModal({
<h4>{validationResult?.name}</h4> <h4>{validationResult?.name}</h4>
<small> <small>
{t("found_download_option", { {t("found_download_option", {
count: validationResult?.downloadCount, count: validationResult?.downloads.length,
countFormatted: countFormatted:
validationResult?.downloadCount.toLocaleString(), validationResult?.downloads.length.toLocaleString(),
})} })}
</small> </small>
</div> </div>

View File

@ -10,8 +10,9 @@ import { AddDownloadSourceModal } from "./add-download-source-modal";
import { useToast } from "@renderer/hooks"; import { useToast } from "@renderer/hooks";
import { DownloadSourceStatus } from "@shared"; import { DownloadSourceStatus } from "@shared";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { settingsContext } from "@renderer/context"; import { repacksContext, settingsContext } from "@renderer/context";
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie"; import { downloadSourcesTable } from "@renderer/dexie";
import { downloadSourcesWorker } from "@renderer/workers";
export function SettingsDownloadSources() { export function SettingsDownloadSources() {
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
@ -19,16 +20,23 @@ export function SettingsDownloadSources() {
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]); const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const [isSyncingDownloadSources, setIsSyncingDownloadSources] = const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
useState(false); useState(false);
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
useState(false);
const { sourceUrl, clearSourceUrl } = useContext(settingsContext); const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { showSuccessToast } = useToast(); const { showSuccessToast } = useToast();
const { indexRepacks } = useContext(repacksContext);
const getDownloadSources = async () => { const getDownloadSources = async () => {
downloadSourcesTable.toArray().then((sources) => { await downloadSourcesTable
setDownloadSources(sources); .toCollection()
}); .sortBy("createdAt")
.then((sources) => {
setDownloadSources(sources.reverse());
});
}; };
useEffect(() => { useEffect(() => {
@ -39,18 +47,23 @@ export function SettingsDownloadSources() {
if (sourceUrl) setShowAddDownloadSourceModal(true); if (sourceUrl) setShowAddDownloadSourceModal(true);
}, [sourceUrl]); }, [sourceUrl]);
const handleRemoveSource = async (id: number) => { const handleRemoveSource = (id: number) => {
await db.transaction("rw", downloadSourcesTable, repacksTable, async () => { setIsRemovingDownloadSource(true);
await downloadSourcesTable.where({ id }).delete(); const channel = new BroadcastChannel(`download_sources:delete:${id}`);
await repacksTable.where({ downloadSourceId: id }).delete();
});
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 () => { const handleAddDownloadSource = async () => {
indexRepacks();
await getDownloadSources(); await getDownloadSources();
showSuccessToast(t("added_download_source")); showSuccessToast(t("added_download_source"));
}; };
@ -59,7 +72,7 @@ export function SettingsDownloadSources() {
setIsSyncingDownloadSources(true); setIsSyncingDownloadSources(true);
window.electron window.electron
.syncDownloadSources() .syncDownloadSources(downloadSources)
.then(() => { .then(() => {
showSuccessToast(t("download_sources_synced")); showSuccessToast(t("download_sources_synced"));
getDownloadSources(); getDownloadSources();
@ -93,7 +106,11 @@ export function SettingsDownloadSources() {
<Button <Button
type="button" type="button"
theme="outline" theme="outline"
disabled={!downloadSources.length || isSyncingDownloadSources} disabled={
!downloadSources.length ||
isSyncingDownloadSources ||
isRemovingDownloadSource
}
onClick={syncDownloadSources} onClick={syncDownloadSources}
> >
<SyncIcon /> <SyncIcon />
@ -104,6 +121,7 @@ export function SettingsDownloadSources() {
type="button" type="button"
theme="outline" theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)} onClick={() => setShowAddDownloadSourceModal(true)}
disabled={isSyncingDownloadSources}
> >
<PlusCircleIcon /> <PlusCircleIcon />
{t("add_download_source")} {t("add_download_source")}
@ -153,6 +171,7 @@ export function SettingsDownloadSources() {
type="button" type="button"
theme="outline" theme="outline"
onClick={() => handleRemoveSource(downloadSource.id)} onClick={() => handleRemoveSource(downloadSource.id)}
disabled={isRemovingDownloadSource}
> >
<NoEntryIcon /> <NoEntryIcon />
{t("remove_download_source")} {t("remove_download_source")}

View File

@ -1,8 +1,63 @@
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie"; import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
import { DownloadSourceStatus } from "@shared";
import type { DownloadSourceValidationResult } from "@types";
self.onmessage = () => { type Payload =
db.transaction("rw", repacksTable, downloadSourcesTable, async () => { | ["IMPORT_DOWNLOAD_SOURCE", DownloadSourceValidationResult & { url: string }]
await repacksTable.where({ downloadSourceId: 10 }).delete(); | ["DELETE_DOWNLOAD_SOURCE", number];
await downloadSourcesTable.where({ id: 10 }).delete();
}); 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);
}
}; };

View File

@ -2,23 +2,6 @@ import MigrationWorker from "./migration.worker?worker";
import RepacksWorker from "./repacks.worker?worker"; import RepacksWorker from "./repacks.worker?worker";
import DownloadSourcesWorker from "./download-sources.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 repacksWorker = new RepacksWorker();
export const downloadSourcesWorker = new DownloadSourcesWorker(); 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);

View File

@ -1,32 +1,43 @@
import { downloadSourcesTable, repacksTable } from "@renderer/dexie"; import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
import { DownloadSource, GameRepack } from "@types"; import { DownloadSource, GameRepack } from "@types";
export type Payload = export type Payload = [DownloadSource[], GameRepack[]];
| ["MIGRATE_REPACKS", GameRepack[]]
| ["MIGRATE_DOWNLOAD_SOURCES", DownloadSource[]];
self.onmessage = async (event: MessageEvent<Payload>) => { self.onmessage = async (event: MessageEvent<Payload>) => {
const [type, data] = event.data; const [downloadSources, gameRepacks] = event.data;
if (type === "MIGRATE_DOWNLOAD_SOURCES") { const downloadSourcesCount = await downloadSourcesTable.count();
const dexieDownloadSources = await downloadSourcesTable.count();
if (data.length !== dexieDownloadSources) { if (downloadSources.length > downloadSourcesCount) {
await downloadSourcesTable.clear(); await db.transaction(
await downloadSourcesTable.bulkAdd(data); "rw",
} downloadSourcesTable,
repacksTable,
self.postMessage("MIGRATE_DOWNLOAD_SOURCES_COMPLETE"); async () => {}
);
} }
if (type === "MIGRATE_REPACKS") { // if (type === "MIGRATE_DOWNLOAD_SOURCES") {
const dexieRepacks = await repacksTable.count(); // const dexieDownloadSources = await downloadSourcesTable.count();
if (data.length !== dexieRepacks) { // if (data.length > dexieDownloadSources) {
await repacksTable.clear(); // await downloadSourcesTable.clear();
await repacksTable.bulkAdd(data); // 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");
// }
}; };

View File

@ -37,11 +37,9 @@ self.onmessage = async (
const results = index.search(formatName(query)).map((index) => { const results = index.search(formatName(query)).map((index) => {
const repack = state.repacks.at(index as number) as SerializedGameRepack; const repack = state.repacks.at(index as number) as SerializedGameRepack;
const uris = JSON.parse(repack.uris);
return { return {
...repack, ...repack,
uris: [...uris, repack.magnet].filter(Boolean), uris: [...repack.uris, repack.magnet].filter(Boolean),
}; };
}); });

View File

@ -224,6 +224,19 @@ export interface UpdateProfileRequest {
bio?: string; 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 { export interface DownloadSource {
id: number; id: number;
name: string; name: string;