diff --git a/README.be.md b/README.be.md
index 9c7dca97..cc6bafb5 100644
--- a/README.be.md
+++ b/README.be.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.cs.md b/README.cs.md
index c64b50fb..7179711a 100644
--- a/README.cs.md
+++ b/README.cs.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Katalog](./docs/screenshot.png)
diff --git a/README.da.md b/README.da.md
new file mode 100644
index 00000000..9f0eb7f7
--- /dev/null
+++ b/README.da.md
@@ -0,0 +1,186 @@
+
+
+
+
+[
![](./resources/icon.png)
](https://hydralauncher.site)
+
+
Hydra Launcher
+
+
+ Hydra er en spil launcher med sin egen indbyggede bittorrent klient.
+
+
+[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
+[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
+
+[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
+[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
+[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
+[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
+[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
+[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
+[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
+[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
+[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
+[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
+
+![Hydra Catalogue](./docs/screenshot.png)
+
+
+
+## Indholdsfortegnelse
+
+- [Indholdsfortegnelse](#indholdsfortegnelse)
+- [Om](#om)
+- [Funktioner](#funktioner)
+- [Installation](#installation)
+- [Bidrag](#-bidrag)
+ - [Bliv medlem af vores Telegram kanal](#-join-our-telegram)
+ - [Fork og klon dit repo](#fork-and-clone-your-repository)
+ - [Måder du kan bidrage](#ways-you-can-contribute)
+ - [Projekt Struktur](#project-structure)
+- [Byg fra kildekode](#build-from-source)
+ - [Installér Node.js](#install-nodejs)
+ - [Installér Yarn](#install-yarn)
+ - [Installér Node Afhængigheder](#install-node-dependencies)
+ - [Installér Python 3.9](#install-python-39)
+ - [Installér Python Afhængigheder](#install-python-dependencies)
+- [Miljøvariabler](#environment-variables)
+- [Køre](#running)
+- [Bygge](#build)
+ - [Bygge bittorrent klienten](#build-the-bittorrent-client)
+ - [Bygge Electron applikationen](#build-the-electron-application)
+- [Bidragere](#contributors)
+- [Licens](#license)
+
+## Om
+
+**Hydra** er en **Spil Launcher** med sin egen indbyggede **BitTorrent Klient**.
+
+Launcheren er skrevet i TypeScript (Electron) og Python, som håndterer torrenting system ved brug af libtorrent.
+
+## Funktioner
+
+- Sin egen indbyggede bittorrent klient
+- How Long To Beat (HLTB) integration på spil siden
+- Downloadsti tilpasning
+- Windows og Linux understøttelse
+- Konstant opdateret
+- Og mere ...
+
+## Installation
+
+Følg trinene her under for at installere:
+
+1. Download den seneste version af Hydra fra [Releases](https://github.com/hydralauncher/hydra/releases/latest) siden.
+ - Download kun .exe hvis du vil installere Hydra på Windows.
+ - Download .deb, .rpm eller .zip hvis du vil installere Hydra på Linux. (afhænger af din Linux distro)
+2. Kør den downloadede fil.
+3. Nyd Hydra!
+
+## Bidrag
+
+### Bliv medlem af vores Telegram kanal
+
+Vi holder vores diskusioner i vores [Telegram](https://t.me/hydralauncher) kanal.
+
+### Fork og klon dit repo
+
+1. Fork repoet [(klik her for at forke nu)](https://github.com/hydralauncher/hydra/fork)
+2. Klon din forkede kode `git clone https://github.com/dit_brugernavn/hydra`
+3. Lav en ny branch
+4. Skub dine commits
+5. Indsend en ny Pull Request
+
+### Måder du kan bidrage
+
+- Oversættelse: Vi vil gerne have at Hydra er tilgængeligt for så mange folk som overhovedet muligt. Du er velkommen til at hjælpe med at oversætte til nye sprog eller at opdatere og forbedre de sprog som allerede er tilgængelige i Hydra.
+- Kode: Hydra er lavet med Typescript, Electron og en lille smule Python. Hvis du har lyst til at bidrage, kan du blive medlem af vores [Telegram](https://t.me/hydralauncher) kanal! (Alt kommunikation foregår hovedsageligt på Engelsk, Brasiliansk eller Russisk)
+
+### Projekt struktur
+
+- torrent-client: Vi bruger libtorrent, et Python bibliotek, til at administrere torrent downloads
+- src/renderer: UI'en i applikationen
+- src/main: her har vi al logikken
+
+## Byg fra kildekode
+
+### Installér Node.js
+
+Vær sikker på at du har Node.js installeret på din maskine. Hvis ikke, kan du downloade og installere det fra [nodejs.org](https://nodejs.org/).
+
+### Installér Yarn
+
+Yarn er et pakkehåndteringsprogram til Node.js. Hvis du ikke har installeret Yarn endnu, så kan du gøre det ved at følge instruktionerne på [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
+
+### Installér Node Afhængigheder
+
+Navigér til projekt mappen og installér Node afhængighederne ved bruge af Yarn:
+
+```bash
+cd hydra
+yarn
+```
+
+### Installér Python 3.9
+
+Vær sikker på at du har Python 3.9 installeret på din maskine. Du kan downloade og installere det her: [python.org](https://www.python.org/downloads/release/python-3913/).
+
+### Installér Python Afhængigheder
+
+Installér de påkrævede Python afhængigheder ved brug af pip:
+
+```bash
+pip install -r requirements.txt
+```
+
+## Miljøvariabler
+
+Du får brug for en SteamGridDB API nøgle for at kunne hente spil ikonerne under installationen.
+
+Når du har det, kan du kopiere og omdøbe `.env.example` filen til `.env` og indsætte nøglen som `STEAMGRIDDB_API_KEY`.
+
+## Køre
+
+Når alt er sat op, kan du køre den følgende kommando for at starte både Electron processen og bittorrent klienten:
+
+```bash
+yarn dev
+```
+
+## Bygge
+
+### Byg bittorrent klienten
+
+Byg bittorrent klienten ved brug af følgende kommando:
+
+```bash
+python torrent-client/setup.py build
+```
+
+### Byg Electron applikationen
+
+Byg Electron applikationen ved brug af følgende kommando:
+
+På Windows:
+
+```bash
+yarn build:win
+```
+
+På Linux:
+
+```bash
+yarn build:linux
+```
+
+## Bidragere
+
+
+
+
+
+## Licens
+
+Hydra benytter sig af [MIT Licensen](LICENSE).
diff --git a/README.de.md b/README.de.md
index a555bf58..1d7f05f8 100644
--- a/README.de.md
+++ b/README.de.md
@@ -23,6 +23,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Katalog](./docs/screenshot.png)
diff --git a/README.es.md b/README.es.md
index 0da6513b..09d8e4e2 100644
--- a/README.es.md
+++ b/README.es.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.fr.md b/README.fr.md
index 223914a7..351b73a9 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Catalogue Hydra](./docs/screenshot.png)
diff --git a/README.it.md b/README.it.md
index f9c36893..b78abe2b 100644
--- a/README.it.md
+++ b/README.it.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.md b/README.md
index cbecb2ff..64612f4b 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.pl.md b/README.pl.md
index 851a0c3f..b4cd5a6a 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.pt-BR.md b/README.pt-BR.md
index 14b2ee68..8eee0c06 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.ru.md b/README.ru.md
index 2ceb0ff7..7bc0d9d8 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/README.uk-UA.md b/README.uk-UA.md
index 0434d0ff..d69ffc21 100644
--- a/README.uk-UA.md
+++ b/README.uk-UA.md
@@ -23,6 +23,7 @@
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
+[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
![Hydra Catalogue](./docs/screenshot.png)
diff --git a/electron-builder.yml b/electron-builder.yml
index a085b1e9..06473566 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -1,4 +1,4 @@
-appId: site.hydralauncher.hydra
+appId: gg.hydralauncher.hydra
productName: Hydra
directories:
buildResources: build
diff --git a/package.json b/package.json
index a9fd7786..6fd3f905 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..81717806 100644
--- a/src/main/events/catalogue/get-games.ts
+++ b/src/main/events/catalogue/get-games.ts
@@ -2,8 +2,7 @@ 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";
+import { steamUrlBuilder } from "@shared";
const getGames = async (
_event: Electron.IpcMainInvokeEvent,
@@ -15,13 +14,14 @@ const getGames = async (
{ name: "list" }
);
- const entries = RepacksManager.findRepacksForCatalogueEntries(
- steamGames.map((game) => convertSteamGameToCatalogueEntry(game))
- );
-
return {
- results: entries,
- cursor: cursor + entries.length,
+ 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/catalogue/get-random-game.ts b/src/main/events/catalogue/get-random-game.ts
index 72b93c33..0a3797a9 100644
--- a/src/main/events/catalogue/get-random-game.ts
+++ b/src/main/events/catalogue/get-random-game.ts
@@ -3,32 +3,15 @@ import { shuffle } from "lodash-es";
import { getSteam250List } from "@main/services";
import { registerEvent } from "../register-event";
-import { getSteamGameById } from "../helpers/search-games";
import type { Steam250Game } from "@types";
const state = { games: Array(), index: 0 };
-const filterGames = async (games: Steam250Game[]) => {
- const results: Steam250Game[] = [];
-
- for (const game of games) {
- const steamGame = await getSteamGameById(game.objectID);
-
- if (steamGame?.repacks.length) {
- results.push(game);
- }
- }
-
- return results;
-};
-
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
if (state.games.length == 0) {
const steam250List = await getSteam250List();
- const filteredSteam250List = await filterGames(steam250List);
-
- state.games = shuffle(filteredSteam250List);
+ state.games = shuffle(steam250List);
}
if (state.games.length == 0) {
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/catalogue/search-games.ts b/src/main/events/catalogue/search-games.ts
index ebe601f2..8f81d40e 100644
--- a/src/main/events/catalogue/search-games.ts
+++ b/src/main/events/catalogue/search-games.ts
@@ -1,7 +1,7 @@
import { registerEvent } from "../register-event";
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
import { CatalogueEntry } from "@types";
-import { HydraApi, RepacksManager } from "@main/services";
+import { HydraApi } from "@main/services";
const searchGamesEvent = async (
_event: Electron.IpcMainInvokeEvent,
@@ -11,15 +11,13 @@ const searchGamesEvent = async (
{ objectId: string; title: string; shop: string }[]
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
- const steamGames = games.map((game) => {
+ return games.map((game) => {
return convertSteamGameToCatalogueEntry({
id: Number(game.objectId),
name: game.title,
clientIcon: null,
});
});
-
- return RepacksManager.findRepacksForCatalogueEntries(steamGames);
};
registerEvent("searchGames", searchGamesEvent);
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 b0c0e470..00000000
--- a/src/main/events/download-sources/add-download-source.ts
+++ /dev/null
@@ -1,42 +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";
-import { RepacksManager } from "@main/services";
-
-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;
- }
- );
-
- await RepacksManager.updateRepacks();
-
- return downloadSource;
-};
-
-registerEvent("addDownloadSource", addDownloadSource);
diff --git a/src/main/events/download-sources/delete-download-source.ts b/src/main/events/download-sources/delete-download-source.ts
new file mode 100644
index 00000000..abfbf661
--- /dev/null
+++ b/src/main/events/download-sources/delete-download-source.ts
@@ -0,0 +1,9 @@
+import { registerEvent } from "../register-event";
+import { knexClient } from "@main/knex-client";
+
+const deleteDownloadSource = async (
+ _event: Electron.IpcMainInvokeEvent,
+ id: number
+) => knexClient("download_source").where({ id }).delete();
+
+registerEvent("deleteDownloadSource", deleteDownloadSource);
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
deleted file mode 100644
index 73f2ffbe..00000000
--- a/src/main/events/download-sources/remove-download-source.ts
+++ /dev/null
@@ -1,13 +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
-) => {
- await downloadSourceRepository.delete(id);
- await RepacksManager.updateRepacks();
-};
-
-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
deleted file mode 100644
index 2e000e64..00000000
--- a/src/main/events/download-sources/sync-download-sources.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { registerEvent } from "../register-event";
-import { fetchDownloadSourcesAndUpdate } from "@main/helpers";
-
-const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
- fetchDownloadSourcesAndUpdate();
-
-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
deleted file mode 100644
index fdb67961..00000000
--- a/src/main/events/download-sources/validate-download-source.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { registerEvent } from "../register-event";
-import { downloadSourceRepository } from "@main/repository";
-import { RepacksManager } from "@main/services";
-import { downloadSourceWorker } from "@main/workers";
-
-const validateDownloadSource = async (
- _event: Electron.IpcMainInvokeEvent,
- url: string
-) => {
- const existingSource = await downloadSourceRepository.findOne({
- where: { url },
- });
-
- 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/helpers/search-games.ts b/src/main/events/helpers/search-games.ts
index 5fb5098e..1f1fc756 100644
--- a/src/main/events/helpers/search-games.ts
+++ b/src/main/events/helpers/search-games.ts
@@ -1,7 +1,6 @@
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
import { steamGamesWorker } from "@main/workers";
-import { RepacksManager } from "@main/services";
import { steamUrlBuilder } from "@shared";
export interface SearchGamesArgs {
@@ -17,7 +16,6 @@ export const convertSteamGameToCatalogueEntry = (
title: game.name,
shop: "steam" as GameShop,
cover: steamUrlBuilder.library(String(game.id)),
- repacks: [],
});
export const getSteamGameById = async (
@@ -29,9 +27,5 @@ export const getSteamGameById = async (
if (!steamGame) return null;
- const catalogueEntry = convertSteamGameToCatalogueEntry(steamGame);
-
- const result = RepacksManager.findRepacksForCatalogueEntry(catalogueEntry);
-
- return result;
+ return convertSteamGameToCatalogueEntry(steamGame);
};
diff --git a/src/main/events/helpers/validators.ts b/src/main/events/helpers/validators.ts
deleted file mode 100644
index ee36bb85..00000000
--- a/src/main/events/helpers/validators.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { z } from "zod";
-
-export const downloadSourceSchema = z.object({
- name: z.string().max(255),
- downloads: z.array(
- z.object({
- title: z.string().max(255),
- uris: z.array(z.string()),
- uploadDate: z.string().max(255),
- fileSize: z.string().max(255),
- })
- ),
-});
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index da11ccc3..22eb341f 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -7,7 +7,6 @@ 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-game-achievements";
@@ -38,11 +37,8 @@ import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
+import "./download-sources/delete-download-source";
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";
import "./auth/get-session-hash";
@@ -61,6 +57,7 @@ import "./profile/update-profile";
import "./profile/process-profile-image";
import "./profile/send-friend-request";
import "./profile/sync-friend-requests";
+import "./notifications/publish-new-repacks-notification";
import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong");
diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts
index 20c9a6e9..69045d46 100644
--- a/src/main/events/library/add-game-to-library.ts
+++ b/src/main/events/library/add-game-to-library.ts
@@ -3,7 +3,6 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
-import { getFileBase64 } from "@main/helpers";
import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync";
@@ -37,20 +36,12 @@ const addGameToLibrary = async (
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
: null;
- await gameRepository
- .insert({
- title,
- iconUrl,
- objectID,
- shop,
- })
- .then(() => {
- if (iconUrl) {
- getFileBase64(iconUrl).then((base64) =>
- gameRepository.update({ objectID }, { iconUrl: base64 })
- );
- }
- });
+ await gameRepository.insert({
+ title,
+ iconUrl,
+ objectID,
+ shop,
+ });
}
updateLocalUnlockedAchivements(true, objectID);
diff --git a/src/main/events/notifications/publish-new-repacks-notification.ts b/src/main/events/notifications/publish-new-repacks-notification.ts
new file mode 100644
index 00000000..5230c209
--- /dev/null
+++ b/src/main/events/notifications/publish-new-repacks-notification.ts
@@ -0,0 +1,29 @@
+import { Notification } from "electron";
+import { registerEvent } from "../register-event";
+import { userPreferencesRepository } from "@main/repository";
+import { t } from "i18next";
+
+const publishNewRepacksNotification = async (
+ _event: Electron.IpcMainInvokeEvent,
+ newRepacksCount: number
+) => {
+ if (newRepacksCount < 1) return;
+
+ const userPreferences = await userPreferencesRepository.findOne({
+ where: { id: 1 },
+ });
+
+ if (userPreferences?.repackUpdatesNotificationsEnabled) {
+ new Notification({
+ title: t("repack_list_updated", {
+ ns: "notifications",
+ }),
+ body: t("repack_count", {
+ ns: "notifications",
+ count: newRepacksCount,
+ }),
+ }).show();
+ }
+};
+
+registerEvent("publishNewRepacksNotification", publishNewRepacksNotification);
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index 253ab159..a2c51a01 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -1,7 +1,6 @@
import { registerEvent } from "../register-event";
import type { StartGameDownloadPayload } from "@types";
-import { getFileBase64 } from "@main/helpers";
import { DownloadManager, HydraApi, logger } from "@main/services";
import { Not } from "typeorm";
@@ -9,36 +8,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();
@@ -71,26 +59,16 @@ const startGameDownload = async (
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
: null;
- await gameRepository
- .insert({
- title,
- iconUrl,
- objectID,
- downloader,
- shop,
- status: "active",
- downloadPath,
- uri,
- })
- .then((result) => {
- if (iconUrl) {
- getFileBase64(iconUrl).then((base64) =>
- gameRepository.update({ objectID }, { iconUrl: base64 })
- );
- }
-
- return result;
- });
+ await gameRepository.insert({
+ title,
+ iconUrl,
+ objectID,
+ downloader,
+ shop,
+ status: "active",
+ downloadPath,
+ uri,
+ });
}
const updatedGame = await gameRepository.findOne({
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..bf29762a 100644
--- a/src/main/helpers/index.ts
+++ b/src/main/helpers/index.ts
@@ -7,16 +7,6 @@ export const getFileBuffer = async (url: string) =>
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
);
-export const getFileBase64 = async (url: string) =>
- fetch(url, { method: "GET" }).then((response) =>
- response.arrayBuffer().then((buffer) => {
- const base64 = Buffer.from(buffer).toString("base64");
- const contentType = response.headers.get("content-type");
-
- return `data:${contentType};base64,${base64}`;
- })
- );
-
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
@@ -36,6 +26,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/index.ts b/src/main/index.ts
index 1f7d8b13..42080d1f 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1,4 +1,4 @@
-import { app, BrowserWindow, net, protocol } from "electron";
+import { app, BrowserWindow, net, protocol, session } from "electron";
import { init } from "@sentry/electron/main";
import updater from "electron-updater";
import i18n from "i18next";
@@ -68,14 +68,13 @@ const runMigrations = async () => {
});
await knexClient.migrate.latest(migrationConfig);
- await knexClient.destroy();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
- electronApp.setAppUserModelId("site.hydralauncher.hydra");
+ electronApp.setAppUserModelId("gg.hydralauncher.hydra");
protocol.handle("local", (request) => {
const filePath = request.url.slice("local:".length);
@@ -105,6 +104,46 @@ app.whenReady().then(async () => {
WindowManager.createMainWindow();
WindowManager.createNotificationWindow();
WindowManager.createSystemTray(userPreferences?.language || "en");
+
+ session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
+ callback({
+ requestHeaders: {
+ ...details.requestHeaders,
+ "user-agent":
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
+ },
+ });
+ });
+
+ session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
+ const headers = {
+ "access-control-allow-origin": ["*"],
+ "access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
+ "access-control-expose-headers": ["ETag"],
+ "access-control-allow-headers": [
+ "Content-Type, Authorization, X-Requested-With, If-None-Match",
+ ],
+ "access-control-allow-credentials": ["true"],
+ };
+
+ if (details.method === "OPTIONS") {
+ callback({
+ cancel: false,
+ responseHeaders: {
+ ...details.responseHeaders,
+ ...headers,
+ },
+ statusLine: "HTTP/1.1 200 OK",
+ });
+ } else {
+ callback({
+ responseHeaders: {
+ ...details.responseHeaders,
+ ...headers,
+ },
+ });
+ }
+ });
});
app.on("browser-window-created", (_, window) => {
diff --git a/src/main/main.ts b/src/main/main.ts
index af594e20..7f3d6370 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -1,25 +1,14 @@
-import {
- DownloadManager,
- RepacksManager,
- PythonInstance,
- startMainLoop,
-} from "./services";
+import { DownloadManager, PythonInstance, startMainLoop } from "./services";
import {
downloadQueueRepository,
- 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 { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
const loadState = async (userPreferences: UserPreferences | null) => {
- RepacksManager.updateRepacks();
-
import("./events");
if (userPreferences?.realDebridApiToken) {
@@ -46,18 +35,6 @@ const loadState = async (userPreferences: UserPreferences | null) => {
}
startMainLoop();
-
- const now = new Date();
-
- fetchDownloadSourcesAndUpdate().then(async () => {
- const newRepacksCount = await repackRepository.count({
- where: {
- createdAt: MoreThan(now),
- },
- });
-
- if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
- });
};
userPreferencesRepository
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index 255b3871..8664062f 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -7,5 +7,4 @@ export * from "./download";
export * from "./how-long-to-beat";
export * from "./process-watcher";
export * from "./main-loop";
-export * from "./repacks-manager";
export * from "./hydra-api";
diff --git a/src/main/services/notifications.ts b/src/main/services/notifications.ts
index aa43571d..81d9e582 100644
--- a/src/main/services/notifications.ts
+++ b/src/main/services/notifications.ts
@@ -49,24 +49,6 @@ export const publishDownloadCompleteNotification = async (game: Game) => {
}
};
-export const publishNewRepacksNotifications = async (count: number) => {
- const userPreferences = await userPreferencesRepository.findOne({
- where: { id: 1 },
- });
-
- if (userPreferences?.repackUpdatesNotificationsEnabled) {
- new Notification({
- title: t("repack_list_updated", {
- ns: "notifications",
- }),
- body: t("repack_count", {
- ns: "notifications",
- count: count,
- }),
- }).show();
- }
-};
-
export const publishNotificationUpdateReadyToInstall = async (
version: string
) => {
diff --git a/src/main/services/repacks-manager.ts b/src/main/services/repacks-manager.ts
deleted file mode 100644
index 933d7431..00000000
--- a/src/main/services/repacks-manager.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { repackRepository } from "@main/repository";
-import { formatName } from "@shared";
-import { CatalogueEntry, GameRepack } from "@types";
-import flexSearch from "flexsearch";
-
-export class RepacksManager {
- public static repacks: GameRepack[] = [];
- private static repacksIndex = new flexSearch.Index();
-
- public static async updateRepacks() {
- this.repacks = await repackRepository
- .find({
- order: {
- createdAt: "DESC",
- },
- })
- .then((repacks) =>
- repacks.map((repack) => {
- const uris: string[] = [];
- const magnet = repack?.magnet;
-
- if (magnet) uris.push(magnet);
-
- return {
- ...repack,
- uris: [...uris, ...JSON.parse(repack.uris)],
- };
- })
- );
-
- for (let i = 0; i < this.repacks.length; i++) {
- this.repacksIndex.remove(i);
- }
-
- this.repacksIndex = new flexSearch.Index();
-
- for (let i = 0; i < this.repacks.length; i++) {
- const repack = this.repacks[i];
-
- const formattedTitle = formatName(repack.title);
-
- this.repacksIndex.add(i, formattedTitle);
- }
- }
-
- public static search(options: flexSearch.SearchOptions) {
- return this.repacksIndex
- .search({ ...options, query: formatName(options.query ?? "") })
- .map((index) => this.repacks[index]);
- }
-
- public static findRepacksForCatalogueEntry(entry: CatalogueEntry) {
- const repacks = this.search({ query: formatName(entry.title) });
- return { ...entry, repacks };
- }
-
- public static findRepacksForCatalogueEntries(entries: CatalogueEntry[]) {
- return entries.map((entry) => {
- const repacks = this.search({ query: formatName(entry.title) });
- return { ...entry, repacks };
- });
- }
-}
diff --git a/src/main/workers/download-source.worker.ts b/src/main/workers/download-source.worker.ts
deleted file mode 100644
index 5ec37c7f..00000000
--- a/src/main/workers/download-source.worker.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { downloadSourceSchema } from "@main/events/helpers/validators";
-import { DownloadSourceStatus } from "@shared";
-import type { DownloadSource, GameRepack } from "@types";
-import axios, { AxiosError, AxiosHeaders } from "axios";
-import { z } from "zod";
-
-export type DownloadSourceResponse = z.infer & {
- etag: string | null;
- status: DownloadSourceStatus;
-};
-
-export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
- const results: DownloadSourceResponse[] = [];
-
- for (const downloadSource of downloadSources) {
- const headers = new AxiosHeaders();
-
- if (downloadSource.etag) {
- headers.set("If-None-Match", downloadSource.etag);
- }
-
- try {
- const response = await axios.get(downloadSource.url, {
- headers,
- });
-
- const source = downloadSourceSchema.parse(response.data);
-
- results.push({
- ...downloadSource,
- downloads: source.downloads,
- etag: response.headers["etag"],
- status: DownloadSourceStatus.UpToDate,
- });
- } catch (err: unknown) {
- const isNotModified = (err as AxiosError).response?.status === 304;
-
- results.push({
- ...downloadSource,
- downloads: [],
- etag: null,
- status: isNotModified
- ? DownloadSourceStatus.UpToDate
- : DownloadSourceStatus.Errored,
- });
- }
- }
-
- return results;
-};
-
-export const validateDownloadSource = async ({
- url,
- repacks,
-}: {
- url: string;
- repacks: GameRepack[];
-}) => {
- 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,
- };
-};
diff --git a/src/main/workers/index.ts b/src/main/workers/index.ts
index b0f9721f..eded03a3 100644
--- a/src/main/workers/index.ts
+++ b/src/main/workers/index.ts
@@ -1,6 +1,5 @@
import path from "node:path";
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
-import downloadSourceWorkerPath from "./download-source.worker?modulePath";
import Piscina from "piscina";
@@ -13,7 +12,3 @@ export const steamGamesWorker = new Piscina({
},
maxThreads: 1,
});
-
-export const downloadSourceWorker = new Piscina({
- filename: downloadSourceWorkerPath,
-});
diff --git a/src/preload/index.ts b/src/preload/index.ts
index ddc72721..91cfc2f7 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -81,13 +81,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Download sources */
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"),
+ deleteDownloadSource: (id: number) =>
+ ipcRenderer.invoke("deleteDownloadSource", id),
/* Library */
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
@@ -203,4 +198,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-signout", listener);
return () => ipcRenderer.removeListener("on-signout", listener);
},
+
+ /* Notifications */
+ publishNewRepacksNotification: (newRepacksCount: number) =>
+ ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
});
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index 5b9e44ca..37e63154 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,6 +26,8 @@ import {
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
+import { downloadSourcesWorker } from "./workers";
+import { repacksContext } from "./context";
export interface AppProps {
children: React.ReactNode;
@@ -37,8 +39,12 @@ export function App() {
const { t } = useTranslation("app");
+ const downloadSourceMigrationLock = useRef(false);
+
const { clearDownload, setLastPacket } = useDownload();
+ const { indexRepacks } = useContext(repacksContext);
+
const {
isFriendsModalVisible,
friendRequetsModalTab,
@@ -197,7 +203,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, {
@@ -206,6 +212,49 @@ export function App() {
});
}, [dispatch, draggingDisabled]);
+ useEffect(() => {
+ if (downloadSourceMigrationLock.current) return;
+
+ downloadSourceMigrationLock.current = true;
+
+ window.electron.getDownloadSources().then(async (downloadSources) => {
+ if (!downloadSources.length) {
+ const id = crypto.randomUUID();
+ const channel = new BroadcastChannel(`download_sources:sync:${id}`);
+
+ channel.onmessage = (event: MessageEvent) => {
+ const newRepacksCount = event.data;
+ window.electron.publishNewRepacksNotification(newRepacksCount);
+ };
+
+ downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
+ }
+
+ for (const downloadSource of downloadSources) {
+ const channel = new BroadcastChannel(
+ `download_sources:import:${downloadSource.url}`
+ );
+ await new Promise((resolve) => {
+ downloadSourcesWorker.postMessage([
+ "IMPORT_DOWNLOAD_SOURCE",
+ downloadSource.url,
+ ]);
+
+ channel.onmessage = () => {
+ window.electron.deleteDownloadSource(downloadSource.id).then(() => {
+ resolve(true);
+ });
+
+ indexRepacks();
+ channel.close();
+ };
+ }).catch(() => channel.close());
+ }
+
+ downloadSourceMigrationLock.current = false;
+ });
+ }, [indexRepacks]);
+
const handleToastClose = useCallback(() => {
dispatch(closeToast());
}, [dispatch]);
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 601b86c7..3993a30d 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";
@@ -17,6 +23,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,
@@ -54,7 +61,6 @@ export function GameDetailsContextProvider({
const { objectID, shop } = useParams();
const [shopDetails, setShopDetails] = useState(null);
- const [repacks, setRepacks] = useState([]);
const [achievements, setAchievements] = useState([]);
const [game, setGame] = useState(null);
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
@@ -67,10 +73,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();
@@ -94,42 +112,41 @@ 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),
- window.electron.getGameAchievements(objectID!, shop as GameShop),
- ])
- .then(([appDetailsResult, repacksResult, statsResult, achievements]) => {
- if (appDetailsResult.status === "fulfilled") {
- setShopDetails(appDetailsResult.value);
+ )
+ .then((result) => {
+ setShopDetails(result);
- if (
- appDetailsResult.value?.content_descriptors.ids.includes(
- SteamContentDescriptor.AdultOnlySexualContent
- )
- ) {
- setHasNSFWContentBlocked(true);
- }
- }
-
- if (repacksResult.status === "fulfilled")
- setRepacks(repacksResult.value);
-
- if (statsResult.status === "fulfilled") setStats(statsResult.value);
-
- if (achievements.status === "fulfilled") {
- setAchievements(achievements.value);
+ if (
+ result?.content_descriptors.ids.includes(
+ SteamContentDescriptor.AdultOnlySexualContent
+ )
+ ) {
+ setHasNSFWContentBlocked(true);
}
})
.finally(() => {
setIsLoading(false);
});
+ window.electron
+ .getGameStats(objectID!, shop as GameShop)
+ .then((result) => {
+ setStats(result);
+ })
+ .catch(() => {});
+
+ window.electron
+ .getGameAchievements(objectID!, shop as GameShop)
+ .then((achievements) => {
+ setAchievements(achievements);
+ })
+ .catch(() => {});
+
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..cddbb209
--- /dev/null
+++ b/src/renderer/src/context/repacks/repacks.context.tsx
@@ -0,0 +1,67 @@
+import type { GameRepack } from "@types";
+import { createContext, useCallback, useEffect, useState } from "react";
+
+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,
+});
+
+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);
+ channel.close();
+ };
+
+ return [];
+ });
+ }, []);
+
+ const indexRepacks = useCallback(() => {
+ setIsIndexingRepacks(true);
+ repacksWorker.postMessage("INDEX_REPACKS");
+
+ repacksWorker.onmessage = () => {
+ setIsIndexingRepacks(false);
+ };
+ }, []);
+
+ useEffect(() => {
+ indexRepacks();
+ }, [indexRepacks]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 082d48e0..71909d41 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -115,12 +115,7 @@ declare global {
/* Download sources */
getDownloadSources: () => Promise;
- validateDownloadSource: (
- url: string
- ) => Promise<{ name: string; downloadCount: number }>;
- addDownloadSource: (url: string) => Promise;
- removeDownloadSource: (id: number) => Promise;
- syncDownloadSources: () => Promise;
+ deleteDownloadSource: (id: number) => Promise;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise;
@@ -184,6 +179,9 @@ declare global {
action: FriendRequestAction
) => Promise;
sendFriendRequest: (userId: string) => Promise;
+
+ /* Notifications */
+ publishNewRepacksNotification: (newRepacksCount: number) => Promise;
}
interface Window {
diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts
new file mode 100644
index 00000000..23f0bf83
--- /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, uris, 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/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts
index d92038aa..50e2fad9 100644
--- a/src/renderer/src/hooks/use-user-details.ts
+++ b/src/renderer/src/hooks/use-user-details.ts
@@ -90,19 +90,9 @@ export function useUserDetails() {
username: userDetails?.username || "",
});
},
- [updateUserDetails]
+ [updateUserDetails, userDetails?.username]
);
- const fetchFriendRequests = useCallback(async () => {
- return window.electron
- .getFriendRequests()
- .then((friendRequests) => {
- syncFriendRequests();
- dispatch(setFriendRequests(friendRequests));
- })
- .catch(() => {});
- }, [dispatch]);
-
const syncFriendRequests = useCallback(async () => {
return window.electron
.syncFriendRequests()
@@ -112,6 +102,16 @@ export function useUserDetails() {
.catch(() => {});
}, [dispatch]);
+ const fetchFriendRequests = useCallback(async () => {
+ return window.electron
+ .getFriendRequests()
+ .then((friendRequests) => {
+ syncFriendRequests();
+ dispatch(setFriendRequests(friendRequests));
+ })
+ .catch(() => {});
+ }, [dispatch, syncFriendRequests]);
+
const showFriendsModal = useCallback(
(initialTab: UserFriendModalTab, userId: string) => {
dispatch(setFriendsModalVisible({ initialTab, userId }));
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index b453b567..3478273b 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -30,6 +30,9 @@ import { store } from "./store";
import resources from "@locales";
import { Achievemnt } from "./pages/achievement/achievement";
+import "./workers";
+import { RepacksContextProvider } from "./context";
+
Sentry.init({});
i18n
@@ -55,20 +58,22 @@ i18n
ReactDOM.createRoot(document.getElementById("root")!).render(
-
-
- }>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
index 0d1b9c1d..9d8a1a11 100644
--- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
@@ -1,6 +1,5 @@
-import { useCallback, useContext, useEffect, useMemo, useState } from "react";
+import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
-import parseTorrent from "parse-torrent";
import { Badge, Button, Modal, TextField } from "@renderer/components";
import type { GameRepack } from "@types";
@@ -33,8 +32,6 @@ export function RepacksModal({
const [repack, setRepack] = useState(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
- const [infoHash, setInfoHash] = useState(null);
-
const { repacks, game } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
@@ -43,18 +40,9 @@ export function RepacksModal({
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
}, [repacks]);
- const getInfoHash = useCallback(async () => {
- if (game?.uri?.startsWith("magnet:")) {
- const torrent = await parseTorrent(game?.uri ?? "");
- if (torrent.infoHash) setInfoHash(torrent.infoHash);
- }
- }, [game]);
-
useEffect(() => {
setFilteredRepacks(sortedRepacks);
-
- if (game?.uri) getInfoHash();
- }, [sortedRepacks, visible, game, getInfoHash]);
+ }, [sortedRepacks, visible, game]);
const handleRepackClick = (repack: GameRepack) => {
setRepack(repack);
@@ -77,9 +65,6 @@ export function RepacksModal({
};
const checkIfLastDownloadedOption = (repack: GameRepack) => {
- if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
- if (!game?.uri) return false;
-
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
};
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..5ec22827 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,9 @@ 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;
@@ -39,41 +42,48 @@ 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;
}
+
+ downloadSourcesWorker.postMessage([
+ "VALIDATE_DOWNLOAD_SOURCE",
+ values.url,
+ ]);
+
+ const channel = new BroadcastChannel(
+ `download_sources:validate:${values.url}`
+ );
+
+ channel.onmessage = (
+ event: MessageEvent
+ ) => {
+ setValidationResult(event.data);
+ channel.close();
+ };
+
+ setUrl(values.url);
},
[setError, t]
);
@@ -91,9 +101,21 @@ export function AddDownloadSourceModal({
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
const handleAddDownloadSource = async () => {
- await window.electron.addDownloadSource(url);
- onClose();
- onAddDownloadSource();
+ setIsLoading(true);
+
+ if (validationResult) {
+ const channel = new BroadcastChannel(`download_sources:import:${url}`);
+
+ downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]);
+
+ channel.onmessage = () => {
+ setIsLoading(false);
+
+ onClose();
+ onAddDownloadSource();
+ channel.close();
+ };
+ }
};
return (
@@ -122,7 +144,7 @@ export function AddDownloadSourceModal({
theme="outline"
style={{ alignSelf: "flex-end" }}
onClick={handleSubmit(onSubmit)}
- disabled={isLoading}
+ disabled={isSubmitting || isLoading}
>
{t("validate_download_source")}
diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx
index 1646af22..d2f45329 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.tsx
+++ b/src/renderer/src/pages/settings/settings-download-sources.tsx
@@ -10,7 +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 { repacksContext, settingsContext } from "@renderer/context";
+import { downloadSourcesTable } from "@renderer/dexie";
+import { downloadSourcesWorker } from "@renderer/workers";
export function SettingsDownloadSources() {
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
@@ -18,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 () => {
- return window.electron.getDownloadSources().then((sources) => {
- setDownloadSources(sources);
- });
+ await downloadSourcesTable
+ .toCollection()
+ .sortBy("createdAt")
+ .then((sources) => {
+ setDownloadSources(sources.reverse());
+ });
};
useEffect(() => {
@@ -38,14 +47,24 @@ export function SettingsDownloadSources() {
if (sourceUrl) setShowAddDownloadSourceModal(true);
}, [sourceUrl]);
- const handleRemoveSource = async (id: number) => {
- await window.electron.removeDownloadSource(id);
- showSuccessToast(t("removed_download_source"));
+ const handleRemoveSource = (id: number) => {
+ setIsRemovingDownloadSource(true);
+ const channel = new BroadcastChannel(`download_sources:delete:${id}`);
- getDownloadSources();
+ downloadSourcesWorker.postMessage(["DELETE_DOWNLOAD_SOURCE", id]);
+
+ channel.onmessage = () => {
+ showSuccessToast(t("removed_download_source"));
+
+ getDownloadSources();
+ indexRepacks();
+ setIsRemovingDownloadSource(false);
+ channel.close();
+ };
};
const handleAddDownloadSource = async () => {
+ indexRepacks();
await getDownloadSources();
showSuccessToast(t("added_download_source"));
};
@@ -53,15 +72,17 @@ export function SettingsDownloadSources() {
const syncDownloadSources = async () => {
setIsSyncingDownloadSources(true);
- window.electron
- .syncDownloadSources()
- .then(() => {
- showSuccessToast(t("download_sources_synced"));
- getDownloadSources();
- })
- .finally(() => {
- setIsSyncingDownloadSources(false);
- });
+ const id = crypto.randomUUID();
+ const channel = new BroadcastChannel(`download_sources:sync:${id}`);
+
+ downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
+
+ channel.onmessage = () => {
+ showSuccessToast(t("download_sources_synced"));
+ getDownloadSources();
+ setIsSyncingDownloadSources(false);
+ channel.close();
+ };
};
const statusTitle = {
@@ -88,7 +109,11 @@ export function SettingsDownloadSources() {