From 849b6de6bce76bbf173ed72fa2ffaee2cece67c5 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sat, 21 Sep 2024 21:19:00 +0100 Subject: [PATCH 1/7] feat: adding dexie --- package.json | 1 + src/main/events/catalogue/get-catalogue.ts | 9 +-- .../events/catalogue/get-game-shop-details.ts | 20 ++--- src/main/events/catalogue/get-games.ts | 10 +-- src/main/events/catalogue/get-repacks.ts | 7 ++ .../events/catalogue/search-game-repacks.ts | 9 --- .../download-sources/add-download-source.ts | 3 - .../download-sources/get-download-sources.ts | 8 +- .../remove-download-source.ts | 5 +- src/main/events/helpers/search-games.ts | 1 - src/main/events/index.ts | 2 +- .../events/torrenting/start-game-download.ts | 27 ++----- src/main/index.ts | 1 - src/main/main.ts | 9 +-- src/preload/index.ts | 2 + src/renderer/src/app.tsx | 78 ++++++++++--------- .../src/components/game-card/game-card.tsx | 18 ++++- .../game-details/game-details.context.tsx | 58 ++++++++------ src/renderer/src/context/index.ts | 1 + .../src/context/repacks/repacks.context.tsx | 58 ++++++++++++++ src/renderer/src/declaration.d.ts | 2 + src/renderer/src/dexie.ts | 13 ++++ src/renderer/src/main.tsx | 2 + .../settings/add-download-source-modal.tsx | 4 + .../settings/settings-download-sources.tsx | 9 ++- .../src/workers/download-sources.worker.ts | 8 ++ src/renderer/src/workers/index.ts | 24 ++++++ src/renderer/src/workers/migration.worker.ts | 32 ++++++++ src/renderer/src/workers/repacks.worker.ts | 52 +++++++++++++ src/types/index.ts | 2 - yarn.lock | 5 ++ 31 files changed, 338 insertions(+), 142 deletions(-) create mode 100644 src/main/events/catalogue/get-repacks.ts delete mode 100644 src/main/events/catalogue/search-game-repacks.ts create mode 100644 src/renderer/src/context/repacks/repacks.context.tsx create mode 100644 src/renderer/src/dexie.ts create mode 100644 src/renderer/src/workers/download-sources.worker.ts create mode 100644 src/renderer/src/workers/index.ts create mode 100644 src/renderer/src/workers/migration.worker.ts create mode 100644 src/renderer/src/workers/repacks.worker.ts diff --git a/package.json b/package.json index c9f3885f..c00d3d1b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "color.js": "^1.2.0", "create-desktop-shortcuts": "^1.11.0", "date-fns": "^3.6.0", + "dexie": "^4.0.8", "electron-log": "^5.1.4", "electron-updater": "^6.1.8", "fetch-cookie": "^3.0.1", diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index 8d6183a5..4fdb95bd 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -1,8 +1,8 @@ import type { GameShop } from "@types"; import { registerEvent } from "../register-event"; -import { HydraApi, RepacksManager } from "@main/services"; -import { CatalogueCategory, formatName, steamUrlBuilder } from "@shared"; +import { HydraApi } from "@main/services"; +import { CatalogueCategory, steamUrlBuilder } from "@shared"; import { steamGamesWorker } from "@main/workers"; const getCatalogue = async ( @@ -26,14 +26,9 @@ const getCatalogue = async ( name: "getById", }); - const repacks = RepacksManager.search({ - query: formatName(steamGame.name), - }); - return { title: steamGame.name, shop: game.shop, - repacks, cover: steamUrlBuilder.library(game.objectId), objectID: game.objectId, }; diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts index 0b4535f6..3a435013 100644 --- a/src/main/events/catalogue/get-game-shop-details.ts +++ b/src/main/events/catalogue/get-game-shop-details.ts @@ -45,15 +45,17 @@ const getGameShopDetails = async ( const appDetails = getLocalizedSteamAppDetails(objectID, language).then( (result) => { - gameShopCacheRepository.upsert( - { - objectID, - shop: "steam", - language, - serializedData: JSON.stringify(result), - }, - ["objectID"] - ); + if (result) { + gameShopCacheRepository.upsert( + { + objectID, + shop: "steam", + language, + serializedData: JSON.stringify(result), + }, + ["objectID"] + ); + } return result; } diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index c34451eb..859a0de5 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -2,8 +2,6 @@ import type { CatalogueEntry } from "@types"; import { registerEvent } from "../register-event"; import { steamGamesWorker } from "@main/workers"; -import { convertSteamGameToCatalogueEntry } from "../helpers/search-games"; -import { RepacksManager } from "@main/services"; const getGames = async ( _event: Electron.IpcMainInvokeEvent, @@ -15,13 +13,9 @@ const getGames = async ( { name: "list" } ); - const entries = RepacksManager.findRepacksForCatalogueEntries( - steamGames.map((game) => convertSteamGameToCatalogueEntry(game)) - ); - return { - results: entries, - cursor: cursor + entries.length, + results: steamGames, + cursor: cursor + steamGames.length, }; }; diff --git a/src/main/events/catalogue/get-repacks.ts b/src/main/events/catalogue/get-repacks.ts new file mode 100644 index 00000000..db39fc7e --- /dev/null +++ b/src/main/events/catalogue/get-repacks.ts @@ -0,0 +1,7 @@ +import { registerEvent } from "../register-event"; +import { knexClient } from "@main/knex-client"; + +const getRepacks = (_event: Electron.IpcMainInvokeEvent) => + knexClient.select("*").from("repack"); + +registerEvent("getRepacks", getRepacks); diff --git a/src/main/events/catalogue/search-game-repacks.ts b/src/main/events/catalogue/search-game-repacks.ts deleted file mode 100644 index e3b9c2b5..00000000 --- a/src/main/events/catalogue/search-game-repacks.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RepacksManager } from "@main/services"; -import { registerEvent } from "../register-event"; - -const searchGameRepacks = ( - _event: Electron.IpcMainInvokeEvent, - query: string -) => RepacksManager.search({ query }); - -registerEvent("searchGameRepacks", searchGameRepacks); diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts index b0c0e470..b762c95d 100644 --- a/src/main/events/download-sources/add-download-source.ts +++ b/src/main/events/download-sources/add-download-source.ts @@ -4,7 +4,6 @@ import { DownloadSource } from "@main/entity"; import axios from "axios"; import { downloadSourceSchema } from "../helpers/validators"; import { insertDownloadsFromSource } from "@main/helpers"; -import { RepacksManager } from "@main/services"; const addDownloadSource = async ( _event: Electron.IpcMainInvokeEvent, @@ -34,8 +33,6 @@ const addDownloadSource = async ( } ); - await RepacksManager.updateRepacks(); - return downloadSource; }; diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts index b8565645..97f8a6d8 100644 --- a/src/main/events/download-sources/get-download-sources.ts +++ b/src/main/events/download-sources/get-download-sources.ts @@ -1,11 +1,7 @@ -import { downloadSourceRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { knexClient } from "@main/knex-client"; const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => - downloadSourceRepository.find({ - order: { - createdAt: "DESC", - }, - }); + knexClient.select("*").from("download_source"); registerEvent("getDownloadSources", getDownloadSources); diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts index 73f2ffbe..8d67df13 100644 --- a/src/main/events/download-sources/remove-download-source.ts +++ b/src/main/events/download-sources/remove-download-source.ts @@ -5,9 +5,6 @@ import { RepacksManager } from "@main/services"; const removeDownloadSource = async ( _event: Electron.IpcMainInvokeEvent, id: number -) => { - await downloadSourceRepository.delete(id); - await RepacksManager.updateRepacks(); -}; +) => downloadSourceRepository.delete(id); registerEvent("removeDownloadSource", removeDownloadSource); diff --git a/src/main/events/helpers/search-games.ts b/src/main/events/helpers/search-games.ts index 5fb5098e..58e9bc92 100644 --- a/src/main/events/helpers/search-games.ts +++ b/src/main/events/helpers/search-games.ts @@ -17,7 +17,6 @@ export const convertSteamGameToCatalogueEntry = ( title: game.name, shop: "steam" as GameShop, cover: steamUrlBuilder.library(String(game.id)), - repacks: [], }); export const getSteamGameById = async ( diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 54e63a3b..0638f900 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -7,9 +7,9 @@ import "./catalogue/get-games"; import "./catalogue/get-how-long-to-beat"; import "./catalogue/get-random-game"; import "./catalogue/search-games"; -import "./catalogue/search-game-repacks"; import "./catalogue/get-game-stats"; import "./catalogue/get-trending-games"; +import "./catalogue/get-repacks"; import "./hardware/get-disk-free-space"; import "./library/add-game-to-library"; import "./library/create-game-shortcut"; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 253ab159..491083cb 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -9,36 +9,25 @@ import { steamGamesWorker } from "@main/workers"; import { createGame } from "@main/services/library-sync"; import { steamUrlBuilder } from "@shared"; import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game, Repack } from "@main/entity"; +import { DownloadQueue, Game } from "@main/entity"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, payload: StartGameDownloadPayload ) => { - const { repackId, objectID, title, shop, downloadPath, downloader, uri } = - payload; + const { objectID, title, shop, downloadPath, downloader, uri } = payload; return dataSource.transaction(async (transactionalEntityManager) => { const gameRepository = transactionalEntityManager.getRepository(Game); - const repackRepository = transactionalEntityManager.getRepository(Repack); const downloadQueueRepository = transactionalEntityManager.getRepository(DownloadQueue); - const [game, repack] = await Promise.all([ - gameRepository.findOne({ - where: { - objectID, - shop, - }, - }), - repackRepository.findOne({ - where: { - id: repackId, - }, - }), - ]); - - if (!repack) return; + const game = await gameRepository.findOne({ + where: { + objectID, + shop, + }, + }); await DownloadManager.pauseDownload(); diff --git a/src/main/index.ts b/src/main/index.ts index 00311b46..594220c5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -68,7 +68,6 @@ const runMigrations = async () => { }); await knexClient.migrate.latest(migrationConfig); - await knexClient.destroy(); }; // This method will be called when Electron has finished diff --git a/src/main/main.ts b/src/main/main.ts index af594e20..690282f6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,9 +1,4 @@ -import { - DownloadManager, - RepacksManager, - PythonInstance, - startMainLoop, -} from "./services"; +import { DownloadManager, PythonInstance, startMainLoop } from "./services"; import { downloadQueueRepository, repackRepository, @@ -18,8 +13,6 @@ import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; const loadState = async (userPreferences: UserPreferences | null) => { - RepacksManager.updateRepacks(); - import("./events"); if (userPreferences?.realDebridApiToken) { diff --git a/src/preload/index.ts b/src/preload/index.ts index 0f135b99..38df190d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -49,6 +49,8 @@ contextBridge.exposeInMainWorld("electron", { getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), + /* Meant for Dexie migration */ + getRepacks: () => ipcRenderer.invoke("getRepacks"), /* User preferences */ getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 5b9e44ca..7b1a2c03 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -26,6 +26,10 @@ 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"); export interface AppProps { children: React.ReactNode; @@ -197,7 +201,7 @@ export function App() { useEffect(() => { new MutationObserver(() => { - const modal = document.body.querySelector("[role=modal]"); + const modal = document.body.querySelector("[role=dialog]"); dispatch(toggleDraggingDisabled(Boolean(modal))); }).observe(document.body, { @@ -211,46 +215,48 @@ export function App() { }, [dispatch]); return ( - <> - {window.electron.platform === "win32" && ( -
-

Hydra

-
- )} + + <> + {window.electron.platform === "win32" && ( +
+

Hydra

+
+ )} - - - {userDetails && ( - - )} -
- - -
-
+ )} -
- -
-
-
+
+ - - +
+
+ +
+ +
+
+
+ + + +
); } diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index 7181e9b3..9d54bad8 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -1,13 +1,14 @@ import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; -import type { CatalogueEntry, GameStats } from "@types"; +import type { CatalogueEntry, GameRepack, GameStats } from "@types"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import * as styles from "./game-card.css"; import { useTranslation } from "react-i18next"; import { Badge } from "../badge/badge"; -import { useCallback, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from "react"; import { useFormat } from "@renderer/hooks"; +import { repacksContext } from "@renderer/context"; export interface GameCardProps extends React.DetailedHTMLProps< @@ -25,9 +26,20 @@ export function GameCard({ game, ...props }: GameCardProps) { const { t } = useTranslation("game_card"); const [stats, setStats] = useState(null); + const [repacks, setRepacks] = useState([]); + + const { searchRepacks, isIndexingRepacks } = useContext(repacksContext); + + useEffect(() => { + if (!isIndexingRepacks) { + searchRepacks(game.title).then((repacks) => { + setRepacks(repacks); + }); + } + }, [game, isIndexingRepacks, searchRepacks]); const uniqueRepackers = Array.from( - new Set(game.repacks.map(({ repacker }) => repacker)) + new Set(repacks.map(({ repacker }) => repacker)) ); const handleHover = useCallback(() => { diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index e723779f..120728b1 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -1,4 +1,10 @@ -import { createContext, useCallback, useEffect, useState } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { setHeaderTitle } from "@renderer/features"; @@ -16,6 +22,7 @@ import type { import { useTranslation } from "react-i18next"; import { GameDetailsContext } from "./game-details.context.types"; import { SteamContentDescriptor } from "@shared"; +import { repacksContext } from "../repacks/repacks.context"; export const gameDetailsContext = createContext({ game: null, @@ -52,7 +59,6 @@ export function GameDetailsContextProvider({ const { objectID, shop } = useParams(); const [shopDetails, setShopDetails] = useState(null); - const [repacks, setRepacks] = useState([]); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); @@ -64,10 +70,22 @@ export function GameDetailsContextProvider({ const [showRepacksModal, setShowRepacksModal] = useState(false); const [showGameOptionsModal, setShowGameOptionsModal] = useState(false); + const [repacks, setRepacks] = useState([]); + const [searchParams] = useSearchParams(); const gameTitle = searchParams.get("title")!; + const { searchRepacks, isIndexingRepacks } = useContext(repacksContext); + + useEffect(() => { + if (!isIndexingRepacks) { + searchRepacks(gameTitle).then((repacks) => { + setRepacks(repacks); + }); + } + }, [game, gameTitle, isIndexingRepacks, searchRepacks]); + const { i18n } = useTranslation("game_details"); const dispatch = useAppDispatch(); @@ -91,37 +109,31 @@ export function GameDetailsContextProvider({ }, [updateGame, isGameDownloading, lastPacket?.game.status]); useEffect(() => { - Promise.allSettled([ - window.electron.getGameShopDetails( + window.electron + .getGameShopDetails( objectID!, shop as GameShop, getSteamLanguage(i18n.language) - ), - window.electron.searchGameRepacks(gameTitle), - window.electron.getGameStats(objectID!, shop as GameShop), - ]) - .then(([appDetailsResult, repacksResult, statsResult]) => { - if (appDetailsResult.status === "fulfilled") { - setShopDetails(appDetailsResult.value); + ) + .then((result) => { + setShopDetails(result); - if ( - appDetailsResult.value?.content_descriptors.ids.includes( - SteamContentDescriptor.AdultOnlySexualContent - ) - ) { - setHasNSFWContentBlocked(true); - } + if ( + result?.content_descriptors.ids.includes( + SteamContentDescriptor.AdultOnlySexualContent + ) + ) { + setHasNSFWContentBlocked(true); } - - if (repacksResult.status === "fulfilled") - setRepacks(repacksResult.value); - - if (statsResult.status === "fulfilled") setStats(statsResult.value); }) .finally(() => { setIsLoading(false); }); + window.electron.getGameStats(objectID!, shop as GameShop).then((result) => { + setStats(result); + }); + updateGame(); }, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]); diff --git a/src/renderer/src/context/index.ts b/src/renderer/src/context/index.ts index d9c1c7e4..8d8b9223 100644 --- a/src/renderer/src/context/index.ts +++ b/src/renderer/src/context/index.ts @@ -1,3 +1,4 @@ export * from "./game-details/game-details.context"; export * from "./settings/settings.context"; export * from "./user-profile/user-profile.context"; +export * from "./repacks/repacks.context"; diff --git a/src/renderer/src/context/repacks/repacks.context.tsx b/src/renderer/src/context/repacks/repacks.context.tsx new file mode 100644 index 00000000..a2e4101b --- /dev/null +++ b/src/renderer/src/context/repacks/repacks.context.tsx @@ -0,0 +1,58 @@ +import type { GameRepack } from "@types"; +import { createContext, useCallback, useEffect, useState } from "react"; + +import { repacksWorker } from "@renderer/workers"; + +export interface RepacksContext { + searchRepacks: (query: string) => Promise; + isIndexingRepacks: boolean; +} + +export const repacksContext = createContext({ + searchRepacks: async () => [] as GameRepack[], + isIndexingRepacks: false, +}); + +const { Provider } = repacksContext; +export const { Consumer: RepacksContextConsumer } = repacksContext; + +export interface RepacksContextProps { + children: React.ReactNode; +} + +export function RepacksContextProvider({ children }: RepacksContextProps) { + const [isIndexingRepacks, setIsIndexingRepacks] = useState(true); + + const searchRepacks = useCallback(async (query: string) => { + return new Promise((resolve) => { + const channelId = crypto.randomUUID(); + repacksWorker.postMessage([channelId, query]); + + const channel = new BroadcastChannel(`repacks:search:${channelId}`); + channel.onmessage = (event: MessageEvent) => { + resolve(event.data); + }; + + return []; + }); + }, []); + + useEffect(() => { + repacksWorker.postMessage("INDEX_REPACKS"); + + repacksWorker.onmessage = () => { + setIsIndexingRepacks(false); + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 70b77eec..3673ec08 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -65,6 +65,8 @@ declare global { searchGameRepacks: (query: string) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; getTrendingGames: () => Promise; + /* Meant for Dexie migration */ + getRepacks: () => Promise; /* Library */ addGameToLibrary: ( diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts new file mode 100644 index 00000000..2b9f0aa6 --- /dev/null +++ b/src/renderer/src/dexie.ts @@ -0,0 +1,13 @@ +import { Dexie } from "dexie"; + +export const db = new Dexie("Hydra"); + +db.version(1).stores({ + repacks: `++id, title, uri, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, + downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, +}); + +export const downloadSourcesTable = db.table("downloadSources"); +export const repacksTable = db.table("repacks"); + +db.open(); diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index b98d5ed9..5d9b2197 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -29,6 +29,8 @@ import { store } from "./store"; import resources from "@locales"; +import "./workers"; + Sentry.init({}); i18n diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index 015ee0dc..fba890d1 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -8,6 +8,7 @@ import { useForm } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; +import { downloadSourcesTable } from "@renderer/dexie"; interface AddDownloadSourceModalProps { visible: boolean; @@ -91,6 +92,9 @@ export function AddDownloadSourceModal({ }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]); const handleAddDownloadSource = async () => { + await downloadSourcesTable.add({ + url, + }); await window.electron.addDownloadSource(url); onClose(); onAddDownloadSource(); diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 1646af22..53c14348 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -11,6 +11,7 @@ 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"; export function SettingsDownloadSources() { const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = @@ -25,7 +26,7 @@ export function SettingsDownloadSources() { const { showSuccessToast } = useToast(); const getDownloadSources = async () => { - return window.electron.getDownloadSources().then((sources) => { + downloadSourcesTable.toArray().then((sources) => { setDownloadSources(sources); }); }; @@ -39,7 +40,11 @@ export function SettingsDownloadSources() { }, [sourceUrl]); const handleRemoveSource = async (id: number) => { - await window.electron.removeDownloadSource(id); + await db.transaction("rw", downloadSourcesTable, repacksTable, async () => { + await downloadSourcesTable.where({ id }).delete(); + await repacksTable.where({ downloadSourceId: id }).delete(); + }); + showSuccessToast(t("removed_download_source")); getDownloadSources(); diff --git a/src/renderer/src/workers/download-sources.worker.ts b/src/renderer/src/workers/download-sources.worker.ts new file mode 100644 index 00000000..609b6bcf --- /dev/null +++ b/src/renderer/src/workers/download-sources.worker.ts @@ -0,0 +1,8 @@ +import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie"; + +self.onmessage = () => { + db.transaction("rw", repacksTable, downloadSourcesTable, async () => { + await repacksTable.where({ downloadSourceId: 10 }).delete(); + await downloadSourcesTable.where({ id: 10 }).delete(); + }); +}; diff --git a/src/renderer/src/workers/index.ts b/src/renderer/src/workers/index.ts new file mode 100644 index 00000000..9a5ab920 --- /dev/null +++ b/src/renderer/src/workers/index.ts @@ -0,0 +1,24 @@ +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 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); diff --git a/src/renderer/src/workers/migration.worker.ts b/src/renderer/src/workers/migration.worker.ts new file mode 100644 index 00000000..848dd052 --- /dev/null +++ b/src/renderer/src/workers/migration.worker.ts @@ -0,0 +1,32 @@ +import { downloadSourcesTable, repacksTable } from "@renderer/dexie"; +import { DownloadSource, GameRepack } from "@types"; + +export type Payload = + | ["MIGRATE_REPACKS", GameRepack[]] + | ["MIGRATE_DOWNLOAD_SOURCES", DownloadSource[]]; + +self.onmessage = async (event: MessageEvent) => { + const [type, data] = event.data; + + if (type === "MIGRATE_DOWNLOAD_SOURCES") { + const dexieDownloadSources = await downloadSourcesTable.count(); + + if (data.length !== dexieDownloadSources) { + await downloadSourcesTable.clear(); + await downloadSourcesTable.bulkAdd(data); + } + + 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); + } + + self.postMessage("MIGRATE_REPACKS_COMPLETE"); + } +}; diff --git a/src/renderer/src/workers/repacks.worker.ts b/src/renderer/src/workers/repacks.worker.ts new file mode 100644 index 00000000..0e3a9ce7 --- /dev/null +++ b/src/renderer/src/workers/repacks.worker.ts @@ -0,0 +1,52 @@ +import { repacksTable } from "@renderer/dexie"; +import { formatName } from "@shared"; +import { GameRepack } from "@types"; +import flexSearch from "flexsearch"; + +const index = new flexSearch.Index(); + +const state = { + repacks: [] as any[], +}; + +interface SerializedGameRepack extends Omit { + uris: string; +} + +self.onmessage = async ( + event: MessageEvent<[string, string] | "INDEX_REPACKS"> +) => { + if (event.data === "INDEX_REPACKS") { + repacksTable + .toCollection() + .sortBy("uploadDate") + .then((results) => { + state.repacks = results.reverse(); + + for (let i = 0; i < state.repacks.length; i++) { + const repack = state.repacks[i]; + const formattedTitle = formatName(repack.title); + index.add(i, formattedTitle); + } + + self.postMessage("INDEXING_COMPLETE"); + }); + } else { + const [requestId, query] = event.data; + + 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), + }; + }); + + const channel = new BroadcastChannel(`repacks:search:${requestId}`); + + channel.postMessage(results); + } +}; diff --git a/src/types/index.ts b/src/types/index.ts index 5b961dd6..a8fb3771 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,7 +44,6 @@ export interface CatalogueEntry { title: string; /* Epic Games covers cannot be guessed with objectID */ cover: string; - repacks: GameRepack[]; } export interface UserGame { @@ -71,7 +70,6 @@ export interface Game { status: GameStatus | null; folderName: string; downloadPath: string | null; - repacks: GameRepack[]; progress: number; bytesDownloaded: number; playTimeInMilliseconds: number; diff --git a/yarn.lock b/yarn.lock index 9aa73bd4..14651b4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3638,6 +3638,11 @@ detect-node@^2.0.4: resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +dexie@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-4.0.8.tgz#21fca70686bdaa1d86fad45b6b19316f6a084a1d" + integrity sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" From f860439fb527b8116eec62bcf9453a684ddf275d Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 22 Sep 2024 17:43:05 +0100 Subject: [PATCH 2/7] feat: adding dexie --- src/main/events/catalogue/get-games.ts | 8 +- .../download-sources/add-download-source.ts | 39 ------- .../remove-download-source.ts | 10 -- .../download-sources/sync-download-sources.ts | 12 ++- .../validate-download-source.ts | 23 +--- src/main/events/index.ts | 2 - src/main/events/user/get-user.ts | 1 - src/main/helpers/download-source.ts | 76 ------------- src/main/helpers/index.ts | 4 +- src/main/main.ts | 26 ++--- ...e.worker.ts => download-sources.worker.ts} | 20 +--- src/main/workers/index.ts | 6 +- src/preload/index.ts | 8 +- src/renderer/src/app.tsx | 101 ++++++++++-------- .../src/context/repacks/repacks.context.tsx | 10 +- src/renderer/src/declaration.d.ts | 7 +- src/renderer/src/dexie.ts | 2 +- src/renderer/src/main.tsx | 29 ++--- .../settings/add-download-source-modal.tsx | 71 ++++++------ .../settings/settings-download-sources.tsx | 47 +++++--- .../src/workers/download-sources.worker.ts | 65 ++++++++++- src/renderer/src/workers/index.ts | 19 +--- src/renderer/src/workers/migration.worker.ts | 53 +++++---- src/renderer/src/workers/repacks.worker.ts | 4 +- src/types/index.ts | 13 +++ 25 files changed, 311 insertions(+), 345 deletions(-) delete mode 100644 src/main/events/download-sources/add-download-source.ts delete mode 100644 src/main/events/download-sources/remove-download-source.ts delete mode 100644 src/main/helpers/download-source.ts rename src/main/workers/{download-source.worker.ts => download-sources.worker.ts} (74%) diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index 859a0de5..81717806 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -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, }; }; diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts deleted file mode 100644 index b762c95d..00000000 --- a/src/main/events/download-sources/add-download-source.ts +++ /dev/null @@ -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); diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts deleted file mode 100644 index 8d67df13..00000000 --- a/src/main/events/download-sources/remove-download-source.ts +++ /dev/null @@ -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); diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts index 2e000e64..49380f30 100644 --- a/src/main/events/download-sources/sync-download-sources.ts +++ b/src/main/events/download-sources/sync-download-sources.ts @@ -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); diff --git a/src/main/events/download-sources/validate-download-source.ts b/src/main/events/download-sources/validate-download-source.ts index fdb67961..4f43ca08 100644 --- a/src/main/events/download-sources/validate-download-source.ts +++ b/src/main/events/download-sources/validate-download-source.ts @@ -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); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 5e2c17a1..73bf38f4 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -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"; diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts index b8bd7a0a..6bbab9c4 100644 --- a/src/main/events/user/get-user.ts +++ b/src/main/events/user/get-user.ts @@ -73,7 +73,6 @@ const getUser = async ( recentGames, }; } catch (err) { - console.log(err); return null; } }; diff --git a/src/main/helpers/download-source.ts b/src/main/helpers/download-source.ts deleted file mode 100644 index c216212a..00000000 --- a/src/main/helpers/download-source.ts +++ /dev/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["downloads"] -) => { - const repacks: QueryDeepPartialEntity[] = 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(); - }); -}; diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 91ce0eb9..a9dcae6c 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -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; diff --git a/src/main/main.ts b/src/main/main.ts index 690282f6..b71bab8c 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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 diff --git a/src/main/workers/download-source.worker.ts b/src/main/workers/download-sources.worker.ts similarity index 74% rename from src/main/workers/download-source.worker.ts rename to src/main/workers/download-sources.worker.ts index 5ec37c7f..c660ad00 100644 --- a/src/main/workers/download-source.worker.ts +++ b/src/main/workers/download-sources.worker.ts @@ -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"], }; }; diff --git a/src/main/workers/index.ts b/src/main/workers/index.ts index b0f9721f..799ed2ef 100644 --- a/src/main/workers/index.ts +++ b/src/main/workers/index.ts @@ -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, }); diff --git a/src/preload/index.ts b/src/preload/index.ts index 38df190d..4d7b7183 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 7b1a2c03..1488cd49 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -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 ( - - <> - {window.electron.platform === "win32" && ( -
-

Hydra

-
- )} + <> + {window.electron.platform === "win32" && ( +
+

Hydra

+
+ )} - + + {userDetails && ( + + )} - {userDetails && ( - + + +
+
- )} -
- +
+ +
+
+ -
-
- -
- -
-
- - - - -
+ + ); } diff --git a/src/renderer/src/context/repacks/repacks.context.tsx b/src/renderer/src/context/repacks/repacks.context.tsx index a2e4101b..c59d5792 100644 --- a/src/renderer/src/context/repacks/repacks.context.tsx +++ b/src/renderer/src/context/repacks/repacks.context.tsx @@ -5,11 +5,13 @@ import { repacksWorker } from "@renderer/workers"; export interface RepacksContext { searchRepacks: (query: string) => Promise; + indexRepacks: () => void; isIndexingRepacks: boolean; } export const repacksContext = createContext({ 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 ( diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 3673ec08..28c5caf7 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -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; validateDownloadSource: ( url: string - ) => Promise<{ name: string; downloadCount: number }>; - addDownloadSource: (url: string) => Promise; - removeDownloadSource: (id: number) => Promise; - syncDownloadSources: () => Promise; + ) => Promise; + syncDownloadSources: (downloadSources: DownloadSource[]) => Promise; /* Hardware */ getDiskFreeSpace: (path: string) => Promise; diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts index 2b9f0aa6..23f0bf83 100644 --- a/src/renderer/src/dexie.ts +++ b/src/renderer/src/dexie.ts @@ -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`, }); diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 5d9b2197..d845e028 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -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( - - - }> - - - - - - - - - - + + + + }> + + + + + + + + + + + ); diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index fba890d1..8e34cbe2 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -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({ resolver: yupResolver(schema), }); - const [validationResult, setValidationResult] = useState<{ - name: string; - downloadCount: number; - } | null>(null); + const [validationResult, setValidationResult] = + useState(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")} @@ -152,9 +159,9 @@ export function AddDownloadSourceModal({

{validationResult?.name}

{t("found_download_option", { - count: validationResult?.downloadCount, + count: validationResult?.downloads.length, countFormatted: - validationResult?.downloadCount.toLocaleString(), + validationResult?.downloads.length.toLocaleString(), })} diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 53c14348..4f70ff6b 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -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([]); 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() {