mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
82 Commits
v2.1.1
...
github/for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63fce8edc | ||
|
|
d93e234001 | ||
|
|
0fdd2797a5 | ||
|
|
4b763173f6 | ||
|
|
ec3748343d | ||
|
|
b4526b6f67 | ||
|
|
a09124e8be | ||
|
|
c7f2a861d5 | ||
|
|
37111c11d8 | ||
|
|
43e9919b6b | ||
|
|
2aa393967f | ||
|
|
17febcd88a | ||
|
|
41c80daaaa | ||
|
|
d57980edc7 | ||
|
|
97dc9b03b0 | ||
|
|
fabeedaa8a | ||
|
|
e701a273d8 | ||
|
|
6c32fdbcbb | ||
|
|
61b2d55d47 | ||
|
|
7b5e4459d4 | ||
|
|
5b450db5eb | ||
|
|
d88e06e289 | ||
|
|
e77991ea16 | ||
|
|
5e55c05bd7 | ||
|
|
81e74f068f | ||
|
|
d5a510175f | ||
|
|
2b2f29da61 | ||
|
|
339dc89702 | ||
|
|
50028fbfd8 | ||
|
|
d97c5b894a | ||
|
|
f860439fb5 | ||
|
|
ddd6ff7dbe | ||
|
|
849b6de6bc | ||
|
|
79e2eb042a | ||
|
|
f9ad26c836 | ||
|
|
f6fbfe33e0 | ||
|
|
18d6ca630a | ||
|
|
c7afcff0b4 | ||
|
|
a3bdfe7641 | ||
|
|
e7b4f8e1c8 | ||
|
|
e526c0f650 | ||
|
|
9791c311f1 | ||
|
|
b1ea9cbfaa | ||
|
|
74d2ec8238 | ||
|
|
f8f2124cec | ||
|
|
3833e11e98 | ||
|
|
613898b32d | ||
|
|
2bc98317e9 | ||
|
|
2e87ae8486 | ||
|
|
df5f34119f | ||
|
|
2271368199 | ||
|
|
e139423b52 | ||
|
|
aa2ecfad96 | ||
|
|
b76441a763 | ||
|
|
2d160ba8ee | ||
|
|
5863b15b12 | ||
|
|
bc175e635f | ||
|
|
f7f89cb778 | ||
|
|
16c45692da | ||
|
|
30aa3f5470 | ||
|
|
ef16732c0a | ||
|
|
84c472a3fa | ||
|
|
2610f8b17b | ||
|
|
705b12019f | ||
|
|
39be8fdf53 | ||
|
|
cc7c3455fa | ||
|
|
d1f4bc7207 | ||
|
|
aa4ca25653 | ||
|
|
405ea0a824 | ||
|
|
43bc0cb08f | ||
|
|
3c200aa2eb | ||
|
|
cc5967814b | ||
|
|
ec16efed2f | ||
|
|
09d0e5b4ef | ||
|
|
5b18aba2b8 | ||
|
|
192008c76c | ||
|
|
1de3a9836c | ||
|
|
ee02811aea | ||
|
|
c21ebe1ce2 | ||
|
|
214e39adda | ||
|
|
8258127616 | ||
|
|
f9906bfe95 |
@@ -1,4 +1,5 @@
|
||||
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
||||
MAIN_VITE_API_URL=API_URL
|
||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
||||
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
|
||||
SENTRY_AUTH_TOKEN=
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
20
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve. Write in English, please.
|
||||
title: "[BUG] "
|
||||
title: "[BUG] Write a title for your bug"
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -29,12 +29,15 @@ body:
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
label: Additional information and data
|
||||
description: |
|
||||
Add screenshots and upload your logs file here.
|
||||
Logs location on Windows: "%appdata%/hydra"
|
||||
Logs location on Linux: "~/.config/hydra/"
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: input
|
||||
id: OS
|
||||
attributes:
|
||||
@@ -49,13 +52,6 @@ body:
|
||||
description: Please provide the version of Hydra you are using.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Please provide any additional information and context about your problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -41,6 +41,7 @@ jobs:
|
||||
yarn build:linux
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -50,6 +51,7 @@ jobs:
|
||||
run: yarn build:win
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -43,6 +43,7 @@ jobs:
|
||||
yarn build:linux
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -52,6 +53,7 @@ jobs:
|
||||
run: yarn build:win
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra - гэта гульнявы лаўнчар з уласным убудаваным кліентам BitTorrent і самастойным scraper`ам для рэпакаў.</strong>
|
||||
<strong>Hydra - гэта гульнявы лаўнчар з уласным убудаваным кліентам BitTorrent.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -139,9 +141,8 @@ pip install -r requirements.txt
|
||||
## Пераменныя асяроддзі
|
||||
|
||||
Вам спатрэбіцца ключ API SteamGridDB, каб атрымаць значкі гульняў пры ўсталёўкі.
|
||||
Калі вы жадаеце выкарыстоўваць onlinefix у якасці рэпака, вам трэба дадаць вашыя ўліковыя дадзеныя ў файл .env.
|
||||
|
||||
Як толькі вы атрымаеце ключ, вы зможаце скапіяваць або пераназваць файл `.env.example` у `.env` і змясціць у яго `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Як толькі вы атрымаеце ключ, вы зможаце скапіяваць або пераназваць файл `.env.example` у `.env` і змясціць у яго `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Запуск
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
|
||||
186
README.da.md
Normal file
186
README.da.md
Normal file
@@ -0,0 +1,186 @@
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
|
||||
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra er en spil launcher med sin egen indbyggede bittorrent klient.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
[](https://github.com/hydralauncher/hydra/releases)
|
||||
|
||||
[](README.pt-BR.md)
|
||||
[](README.md)
|
||||
[](README.ru.md)
|
||||
[](README.uk-UA.md)
|
||||
[](README.be.md)
|
||||
[](README.es.md)
|
||||
[](README.fr.md)
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 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**.
|
||||
<br>
|
||||
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!
|
||||
|
||||
## <a name="bidrag"> Bidrag
|
||||
|
||||
### <a name="join-our-telegram"></a> 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
|
||||
|
||||
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
||||
</a>
|
||||
|
||||
## Licens
|
||||
|
||||
Hydra benytter sig af [MIT Licensen](LICENSE).
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.fr.md)
|
||||
[](README.de.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
|
||||
11
README.es.md
11
README.es.md
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra es un launcher de juegos con su propio cliente de bittorrent y gestor propio de repacks.</strong>
|
||||
<strong>Hydra es un launcher de juegos con su propio cliente de bittorrent.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -55,17 +57,15 @@
|
||||
|
||||
## Acerca de
|
||||
|
||||
**Hydra** es un **Launcher de Juegos** con su propio **Cliente Bittorrent** y **autogestor de Repacks**.
|
||||
**Hydra** es un **Launcher de Juegos** con su propio **Cliente Bittorrent**.
|
||||
<br>
|
||||
El launcher está escrito en TypeScript (Electron) y Python, el cuál se encarga del sistema de torrent usando libtorrent.
|
||||
|
||||
## Caracteristicas
|
||||
|
||||
- Buscador e instalador autogestionado de repacks a través de las páginas más confiables en él [Megahilo](https://www.reddit.com/r/Piracy/wiki/megathread/)
|
||||
- Cliente propio de bittorrent integrado
|
||||
- Integración de How Long To Beat (HLTB) en la página del juego
|
||||
- Customización de rutas de descargas
|
||||
- Notificaciones en actualizaciones a listas de repacks
|
||||
- Soporte a Windows y Linux
|
||||
- En constante actualización
|
||||
- Y mucho más ...
|
||||
@@ -139,9 +139,8 @@ pip install -r requirements.txt
|
||||
## Variables del Entorno
|
||||
|
||||
Necesitas una llave API de SteamGridDB para así poder obtener los íconos de los juegos en la instalación.
|
||||
Si quieres también tener los repacks de onlinefix, necesitarás añadir tus credenciales al .env
|
||||
|
||||
Una vez que los tengas, puedes copiar o renombrar el archivo `.env.example` cómo `.env` y colocarlo en `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Una vez que los tengas, puedes copiar o renombrar el archivo `.env.example` cómo `.env` y colocarlo en `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Ejecucion
|
||||
|
||||
|
||||
11
README.fr.md
11
README.fr.md
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra est un lanceur de jeux avec son propre client bittorrent intégré et un scraper de repack auto-géré.</strong>
|
||||
<strong>Hydra est un lanceur de jeux avec son propre client bittorrent intégré.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -55,17 +57,15 @@
|
||||
|
||||
## À propos
|
||||
|
||||
**Hydra** est un **lanceur de jeux** avec son propre **client BitTorrent** intégré et un **scraper de repack auto-géré**.
|
||||
**Hydra** est un **lanceur de jeux** avec son propre **client BitTorrent** intégré.
|
||||
<br>
|
||||
Le lanceur est écrit en TypeScript (Electron) et Python, qui gère le système de torrent en utilisant libtorrent.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- Scraper de repack auto-géré parmi tous les sites les plus fiables sur le [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
|
||||
- Client bittorrent intégré
|
||||
- Intégration How Long To Beat (HLTB) sur la page du jeu
|
||||
- Personnalisation des chemins de téléchargement
|
||||
- Notifications de mise à jour de la liste de repack
|
||||
- Support pour Windows et Linux
|
||||
- Constamment mis à jour
|
||||
- Et plus encore ...
|
||||
@@ -139,9 +139,8 @@ pip install -r requirements.txt
|
||||
## Variables d'environnement
|
||||
|
||||
Vous aurez besoin d'une clé API SteamGridDB pour récupérer les icônes de jeux lors de l'installation.
|
||||
Si vous voulez avoir onlinefix comme repacker, vous devrez ajouter vos identifiants au fichier .env.
|
||||
|
||||
Une fois que vous l'avez, vous pouvez copier ou renommer le fichier `.env.example` en `.env` et y mettre `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Une fois que vous l'avez, vous pouvez copier ou renommer le fichier `.env.example` en `.env` et y mettre `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Lancement
|
||||
|
||||
|
||||
11
README.it.md
11
README.it.md
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra è un game launcher con il proprio client bittorrent e autogestore di repacks.</strong>
|
||||
<strong>Hydra è un game launcher con il proprio client bittorrent.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -55,17 +57,15 @@
|
||||
|
||||
## A proposito
|
||||
|
||||
**Hydra** è un **Game Launcher** con il proprio **Client BitTorrent** e **autogestore di repack**.
|
||||
**Hydra** è un **Game Launcher** con il proprio **Client BitTorrent**.
|
||||
<br>
|
||||
Il launcher è scritto in TypeScript (Electron) and Python, che gestisce il sistema di torrenting appoggiandosi a libtorrent.
|
||||
|
||||
## Caratteristiche
|
||||
|
||||
- Motore di ricerca automatizzato sulle fonti di repack dal [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
|
||||
- Client Bittorrent integrato
|
||||
- Integrazione How Long To Beat (HLTB) nella pagina del gioco
|
||||
- Percorso del download Personalizzato
|
||||
- Notifiche di aggiornamenti sulla list dei repacks
|
||||
- Supporto Windows e Linux
|
||||
- Costantemente Aggiornato
|
||||
- E molto altro ...
|
||||
@@ -139,9 +139,8 @@ pip install -r requirements.txt
|
||||
## Variabili d'ambiente
|
||||
|
||||
Avrai bisogno di una chiave API SteamGridDB per poter caricare le icone di gioco.
|
||||
Se intendi avere onlinefix come repacker dovrai aggiungere le tue credenziali al file .env
|
||||
|
||||
Una volta ottenuta, puoi copiare e rinominare il file `.env.example` a `.env` e metterlo in `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Una volta ottenuta, puoi copiare e rinominare il file `.env.example` a `.env` e metterlo in `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Esecuzione
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
|
||||
187
README.nb.md
Normal file
187
README.nb.md
Normal file
@@ -0,0 +1,187 @@
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
|
||||
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra er en spill launcher sin egen innebygt bittorrent klient.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
[](https://github.com/hydralauncher/hydra/releases)
|
||||
|
||||
[](README.pt-BR.md)
|
||||
[](README.md)
|
||||
[](README.ru.md)
|
||||
[](README.uk-UA.md)
|
||||
[](README.be.md)
|
||||
[](README.es.md)
|
||||
[](README.fr.md)
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Innhold
|
||||
|
||||
- [Innhold](#innhold)
|
||||
- [Om](#om)
|
||||
- [Funksjoner](#funksjoner)
|
||||
- [Installasjon](#installasjon)
|
||||
- [Bidra](#-bidra)
|
||||
- [Bli med i Telegram kanalen vår](#-join-our-telegram)
|
||||
- [Forke og klone repoet ditt](#fork-and-clone-your-repository)
|
||||
- [Måter du kan bidra](#ways-you-can-contribute)
|
||||
- [Prosjekt Struktur](#project-structure)
|
||||
- [Bygg fra kilden](#build-from-source)
|
||||
- [Installere Node.js](#install-nodejs)
|
||||
- [Installere Yarn](#install-yarn)
|
||||
- [Installere Node-avhengigheter](#install-node-dependencies)
|
||||
- [Installere Python 3.9](#install-python-39)
|
||||
- [Installere Python-avhengigheter](#install-python-dependencies)
|
||||
- [Miljøvariabler](#environment-variables)
|
||||
- [Kjøre](#running)
|
||||
- [Bygge](#build)
|
||||
- [Bygg bittorrent klienten](#build-the-bittorrent-client)
|
||||
- [Bygg Electron applikationen](#build-the-electron-application)
|
||||
- [Bidragsytere](#contributors)
|
||||
- [Lisens](#license)
|
||||
|
||||
## Om
|
||||
|
||||
**Hydra** er en **Spill Launcher** sin egne innbygte **BitTorrent Klient**.
|
||||
<br>
|
||||
Launcheren er skrevet i TypeScript (Electron) og Python, som håndterer torrent systemet ved bruk av libtorrent.
|
||||
|
||||
## Funksjoner
|
||||
|
||||
- Sin egen innebyggte bittorrent klient
|
||||
- How Long To Beat (HLTB) integrasjon på spillsiden
|
||||
- Nedlastingssti tilpasning
|
||||
- Windows og Linux understøttelse
|
||||
- Konstant oppdatert
|
||||
- Og mer ...
|
||||
|
||||
## Installasjon
|
||||
|
||||
Følg trinnene her under for å innstallere:
|
||||
|
||||
1. Last ned den seneste versjonen av Hydra fra [Releases](https://github.com/hydralauncher/hydra/releases/latest) siden.
|
||||
- Last kun .exe filen ned om du vil installere Hydra på Windows.
|
||||
- Last kun .deb, .rpm eller .zip ned om du vil installere Hydra på Linux. (kommer an på Linux distroen din)
|
||||
2. Kjør den nedlastede filen.
|
||||
3. Nyt Hydra!
|
||||
|
||||
## <a name="contributing"> Bidra
|
||||
|
||||
### <a name="join-our-telegram"></a> Bli med i Telegram kanalen vår
|
||||
|
||||
Vi holder diskusjonene våres i [Telegram](https://t.me/hydralauncher) kanalen.
|
||||
|
||||
### Forke og klone repoet ditt
|
||||
|
||||
1. Fork repoet [(trykk her for å forke nå)](https://github.com/hydralauncher/hydra/fork)
|
||||
2. Klon den forkede koden `git clone https://github.com/brukernavnet_ditt/hydra`
|
||||
3. Lag en ny branch
|
||||
4. Skyv committene dine
|
||||
5. Send inn en ny Pull-forespørsel.
|
||||
|
||||
### Måter du kan bidra
|
||||
|
||||
- Oversetting: Vi har lyst at Hydra skal bli tilgjengelig for så mange som mulig. Hjelp gjerne med å oversette til nye språk eller oppdater og forbedre de som allerede er tilgjengelige i Hydra.
|
||||
- Code: Hydra is built with Typescript, Electron and a little bit of Python. If you want to contribute, join our [Telegram](https://t.me/hydralauncher)!
|
||||
- Kode: Hydra er laget med Typescript, Electron og lite gran Pythong. Hvis du har lyst på å bidra, bli med i [Telegram](https://t.me/hydralauncher) kanalen vår!
|
||||
|
||||
### Prosjektstruktur
|
||||
|
||||
- torrent-client: Vi bruker libtorrent, et Python-bibliotek, til å håndtere torrent nedlastinger.
|
||||
- src/renderer: UIen til applikasjonen
|
||||
- src/main: all logikken er her.
|
||||
|
||||
## Bygg fra kildekoden
|
||||
|
||||
### Installere Node.js
|
||||
|
||||
Vær sikker på at du har installert Node.js på maskinen din. Hvis du ikke har det, må du laste ned og installere det fra [nodejs.org](https://nodejs.org/).
|
||||
|
||||
### Installere Yarn
|
||||
|
||||
Yarn er et pakkehåndteringsverktøy til Node.js. Hvis du ikke allerede har installert Yarn, da kan du gjøre det ved å følge instruksjonene på [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
|
||||
|
||||
### Installere Node-avhengigheter
|
||||
|
||||
Naviger til prosjektmappen og installer Node-avhengighetene ved bruk av Yarn:
|
||||
|
||||
```bash
|
||||
cd hydra
|
||||
yarn
|
||||
```
|
||||
|
||||
### Installere Python 3.9
|
||||
|
||||
Vær sikker på at du har installert Python 3.9 på maskinen din. Du kan laste ned og installere det på [python.org](https://www.python.org/downloads/release/python-3913/).
|
||||
|
||||
### Installere Python-avhengigheter
|
||||
|
||||
Installer de nødvendige Python-avhengigheter ved bruk av pip:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Miljøvariabler
|
||||
|
||||
Du trenger en SteamGridDB API nøkkel for å kunne hente spillikonene ved installasjon.
|
||||
|
||||
Når du har det, kan du kopiere eller endre navnet på `.env.example` filen til å være `.env` og lagre nøkkelen som `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Kjøre
|
||||
|
||||
Når alt er satt op, kan du kjøre følgende kommando for å start både Electron prosessen og bittorrent klienten.
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Bygge
|
||||
|
||||
### Bygge bittorrent klienten
|
||||
|
||||
Bygg bittorrent klienten ved å bruke denne kommandoen:
|
||||
|
||||
```bash
|
||||
python torrent-client/setup.py build
|
||||
```
|
||||
|
||||
### Bygge Electron applikasjonen
|
||||
|
||||
Bygg Electron applikasjonen ved å bruke denne kommandoen:
|
||||
|
||||
På Windows:
|
||||
|
||||
```bash
|
||||
yarn build:win
|
||||
```
|
||||
|
||||
På Linux:
|
||||
|
||||
```bash
|
||||
yarn build:linux
|
||||
```
|
||||
|
||||
## Bidragsytere
|
||||
|
||||
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
||||
</a>
|
||||
|
||||
## Lisens
|
||||
|
||||
Hydra bruker [MIT Lisensen](LICENSE).
|
||||
11
README.pl.md
11
README.pl.md
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra - to program uruchamiający gry z własnym wbudowanym klientem bittorrent i samodzielnie zarządzanym repackagerem..</strong>
|
||||
<strong>Hydra - to program uruchamiający gry z własnym wbudowanym klientem bittorrent.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -55,17 +57,15 @@
|
||||
|
||||
## O nas
|
||||
|
||||
**Hydra** - jest **programem uruchamiającym gry** z wbudowanym **klientem BitTorrent** i **samozarządzającym się repackagerem**.
|
||||
**Hydra** - jest **programem uruchamiającym gry** z wbudowanym **klientem BitTorrent**.
|
||||
<br>
|
||||
Ten launcher jest napisany w TypeScript (Electron) i Pythonie, który współpracuje z systemem torrent przy użyciu libtorrent.
|
||||
|
||||
## Cechy
|
||||
|
||||
- Samodzielnie zarządzany repackager wśród wszystkich najbardziej zaufanych stron na [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/").
|
||||
- Własny wbudowany klient bittorrent
|
||||
- Integracja funkcji How Long To Beat (HLTB) na stronie gry
|
||||
- Personalizacja folderu pobierania
|
||||
- Powiadomienia o aktualizacjach listy repacków
|
||||
- Wsparcie dla systemów Windows i Linux
|
||||
- Stała aktualizacja
|
||||
- I nie tylko ...
|
||||
@@ -143,9 +143,8 @@ pip install -r requirements.txt
|
||||
## Zmienne środowiskowe
|
||||
|
||||
Będziesz potrzebował klucza API SteamGridDB, aby uzyskać ikony gier podczas instalacji.
|
||||
Jeśli chcesz użyć onlinefix jako repackagera, musisz dodać swoje dane uwierzytelniające do .env
|
||||
|
||||
Po jego uzyskaniu można skopiować plik lub zmienić jego nazwę `.env.example` na `.env` i umieść go na`STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Po jego uzyskaniu można skopiować plik lub zmienić jego nazwę `.env.example` na `.env` i umieść go na`STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Run
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra é um Launcher de Jogos com seu próprio cliente de bittorrent integrado e um wrapper autogerenciado para busca de repacks.</strong>
|
||||
<strong>Hydra é um Launcher de Jogos com seu próprio cliente de bittorrent integrado.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -55,17 +57,15 @@
|
||||
|
||||
## <a name="about"> Sobre
|
||||
|
||||
**Hydra** é um **Launcher de Jogos** com seu próprio **Cliente BitTorrent incorporado** e um **raspador de repack auto-gerenciado**.
|
||||
**Hydra** é um **Launcher de Jogos** com seu próprio **Cliente BitTorrent incorporado**.
|
||||
<br>
|
||||
O launcher é escrito em TypeScript (Electron) e Python, que lida com o sistema de torrent usando libtorrent.
|
||||
|
||||
## <a name="features"> Recursos
|
||||
|
||||
- Wrapper de repacks auto-gerenciado entre todos os sites mais confiáveis no [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
|
||||
- Cliente BitTorrent incorporado próprio
|
||||
- Integração com [How Long To Beat (HLTB)](https://howlongtobeat.com/) na página do jogo
|
||||
- Personalização do caminho de downloads
|
||||
- Notificações de atualização da lista de repacks
|
||||
- Suporte para Windows e Linux
|
||||
- Constantemente atualizado
|
||||
- E mais ...
|
||||
@@ -136,14 +136,13 @@ Instale as dependências Python necessárias usando o pip:
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## <a name="environment-variables"></a> Environment variables
|
||||
## <a name="environment-variables"></a> Variáveis de ambiente
|
||||
|
||||
Você precisará de uma chave da API SteamGridDB para buscar os ícones do jogo durante a instalação.
|
||||
Se você deseja ter o onlinefix como um repacker, precisará adicionar suas credenciais ao arquivo .env.
|
||||
|
||||
Depois de obtê-lo, você pode copiar ou renomear o arquivo `.env.example` para `.env` e inserir `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME` e `ONLINEFIX_PASSWORD`.
|
||||
Depois de obtê-lo, você pode copiar ou renomear o arquivo `.env.example` para `.env` e inserir `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## <a name="running"></a> Running
|
||||
## <a name="running"></a> Executando
|
||||
|
||||
Uma vez que você tenha configurado tudo, você pode executar o seguinte comando para iniciar tanto o processo Electron quanto o cliente BitTorrent:
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra - это игровой лаунчер с собственным встроенным клиентом BitTorrent и самостоятельным scraper`ом для репаков.</strong>
|
||||
<strong>Hydra - это игровой лаунчер с собственным встроенным клиентом BitTorrent.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -139,9 +141,8 @@ pip install -r requirements.txt
|
||||
## Переменные среды
|
||||
|
||||
Вам понадобится ключ API SteamGridDB, чтобы получить значки игр при установке.
|
||||
Если вы хотите использовать onlinefix в качестве репака, вам нужно добавить ваши учетные данные в файл .env.
|
||||
|
||||
Как только у вас будет ключ, вы можете скопировать или переименовать файл `.env.example` в `.env` и поместить в него `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Как только у вас будет ключ, вы можете скопировать или переименовать файл `.env.example` в `.env` и поместить в него `STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Запуск
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra - це ігровий лаунчер з власним вбудованим bittorrent-клієнтом і самокерованим збирачем репаків.</strong>
|
||||
<strong>Hydra - це ігровий лаунчер з власним вбудованим bittorrent-клієнтом.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
@@ -23,6 +23,8 @@
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
|
||||

|
||||
|
||||
@@ -143,9 +145,8 @@ pip install -r requirements.txt
|
||||
## Змінні середовища
|
||||
|
||||
Вам знадобиться ключ API SteamGridDB, щоб отримати іконки ігор під час встановлення.
|
||||
Якщо ви хочете використовувати onlinefix як перепакувальник, вам потрібно додати свої облікові дані до .env
|
||||
|
||||
Отримавши його, ви можете скопіювати або перейменувати файл `.env.example` на `.env`і помістити його на`STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
|
||||
Отримавши його, ви можете скопіювати або перейменувати файл `.env.example` на `.env`і помістити його на`STEAMGRIDDB_API_KEY`.
|
||||
|
||||
## Запустіть
|
||||
|
||||
|
||||
7
build/installer.nsh
Normal file
7
build/installer.nsh
Normal file
@@ -0,0 +1,7 @@
|
||||
!macro customUnInstall
|
||||
${ifNot} ${isUpdated}
|
||||
RMDir /r "$APPDATA\${APP_PACKAGE_NAME}"
|
||||
RMDir /r "$APPDATA\hydra"
|
||||
RMDir /r "$LOCALAPPDATA\hydralauncher-updater"
|
||||
${endIf}
|
||||
!macroend
|
||||
@@ -1,4 +1,4 @@
|
||||
appId: site.hydralauncher.hydra
|
||||
appId: gg.hydralauncher.hydra
|
||||
productName: Hydra
|
||||
directories:
|
||||
buildResources: build
|
||||
@@ -27,6 +27,7 @@ nsis:
|
||||
createDesktopShortcut: always
|
||||
oneClick: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
include: installer.nsh
|
||||
portable:
|
||||
artifactName: ${name}-${version}-portable.${ext}
|
||||
mac:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.7",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"language_name": "Dansk",
|
||||
"app": {
|
||||
"successfully_signed_in": "Loggede ind successfuldt"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Anbefalet",
|
||||
"trending": "Trender",
|
||||
"surprise_me": "Overrask mig",
|
||||
"no_results": "Ingen resultater fundet"
|
||||
"no_results": "Ingen resultater fundet",
|
||||
"start_typing": "Begynd at skrive for at søge...",
|
||||
"hot": "Populært lige nu",
|
||||
"weekly": "📅 Mest populære spil denne uge"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
@@ -12,27 +18,35 @@
|
||||
"settings": "Indstillinger",
|
||||
"my_library": "Mit bibliotek",
|
||||
"downloading_metadata": "{{title}} (Downloader metadata…)",
|
||||
"paused": "{{title}} (Paused)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filtrer bibliotek",
|
||||
"home": "Hjem"
|
||||
"paused": "{{title}} (Sat på pause)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloader…)",
|
||||
"filter": "Filtrér bibliotek",
|
||||
"home": "Hjem",
|
||||
"queued": "{{title}} (I køen)",
|
||||
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
||||
"sign_in": "Log ind",
|
||||
"friends": "Venner"
|
||||
},
|
||||
"header": {
|
||||
"search": "Søg spil",
|
||||
"search": "Søg efter spil",
|
||||
"home": "Hjem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
"search_results": "Søge resultater",
|
||||
"settings": "Indstillinger"
|
||||
"settings": "Indstillinger",
|
||||
"version_available_install": "Version {{version}} tilgængelig. Klik her for at genstarte og installere.",
|
||||
"version_available_download": "Version {{version}} tilgængelig. Klik her for at downloade."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Ingen downloads igang",
|
||||
"downloading_metadata": "Downloader {{title}} metadata…",
|
||||
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}"
|
||||
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Fuldt downloadet {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…",
|
||||
"checking_files": "Checker {{title}} filer… ({{percentage}} færdig)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Næste side",
|
||||
"previous_page": "Tidligere side"
|
||||
"previous_page": "Forrige side"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Åben download muligheder",
|
||||
@@ -47,11 +61,13 @@
|
||||
"remove": "Fjern",
|
||||
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
||||
"eta": "Konklusion {{eta}}",
|
||||
"calculating_eta": "Udregner resterende tid…",
|
||||
"downloading_metadata": "Downloader metadata…",
|
||||
"filter": "Filtrer repacks",
|
||||
"filter": "Filtrér repacks",
|
||||
"requirements": "System behov",
|
||||
"minimum": "Mindste",
|
||||
"recommended": "Anbefalet",
|
||||
"paused": "Sat på pause",
|
||||
"release_date": "Offentliggjort den {{date}}",
|
||||
"publisher": "Udgivet af {{publisher}}",
|
||||
"hours": "timer",
|
||||
@@ -70,10 +86,51 @@
|
||||
"deleting": "Sletter installatør…",
|
||||
"close": "Luk",
|
||||
"playing_now": "Spiller nu",
|
||||
"change": "Ændré",
|
||||
"change": "Ændre",
|
||||
"repacks_modal_description": "Vælg den repack du vil downloade",
|
||||
"select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne</0>",
|
||||
"download_now": "Download nu"
|
||||
"download_now": "Download nu",
|
||||
"no_shop_details": "Kunne ikke modtage butiks detaljerne.",
|
||||
"download_options": "Download muligheder",
|
||||
"download_path": "Download sti",
|
||||
"previous_screenshot": "Forrige skærmbillede",
|
||||
"next_screenshot": "Næste skærmbillede",
|
||||
"screenshot": "Skærmbillede {{number}}",
|
||||
"open_screenshot": "Åben skærmbillede {{number}}",
|
||||
"download_settings": "Download indstillinger",
|
||||
"downloader": "Downloader",
|
||||
"select_executable": "Vælg",
|
||||
"no_executable_selected": "Ingen eksekverbar fil valgt",
|
||||
"open_folder": "Åben mappe",
|
||||
"open_download_location": "Se downloadede filer",
|
||||
"create_shortcut": "Lav skrivebords genvej",
|
||||
"remove_files": "Fjern filer",
|
||||
"remove_from_library_title": "Er du sikker?",
|
||||
"remove_from_library_description": "Dette vil fjerne {{game}} fra dit bibliotek",
|
||||
"options": "Valgmuligheder",
|
||||
"executable_section_title": "Eksekverbar fil",
|
||||
"executable_section_description": "Sti til filen som skal bruges når \"Spil\" bliver klikket",
|
||||
"downloads_secion_title": "Downloads",
|
||||
"downloads_section_description": "Undersøg opdateringer eller andre versioner af dette spil",
|
||||
"danger_zone_section_title": "Farezonen",
|
||||
"danger_zone_section_description": "Fjern dette spil fra dit bibliotek eller filerne der er blevet downloadet af Hydra",
|
||||
"download_in_progress": "Download undervejs",
|
||||
"download_paused": "Download sat på pause",
|
||||
"last_downloaded_option": "Sidste download mulighed",
|
||||
"create_shortcut_success": "Genvej lavet successfuldt",
|
||||
"create_shortcut_error": "Fejl under skabelsen af genvej",
|
||||
"nsfw_content_title": "Dette spil indeholder upassende indhold",
|
||||
"nsfw_content_description": "{{title}} indeholder indhold der ikke egner sig til alle aldre. Er du sikker på at du vil fortsætte?",
|
||||
"allow_nsfw_content": "Fortsæt",
|
||||
"refuse_nsfw_content": "Gå tilbage",
|
||||
"stats": "Statistik",
|
||||
"download_count": "Downloads",
|
||||
"player_count": "Aktive spillere",
|
||||
"download_error": "Denne download mulighed er ikke tilgængelig",
|
||||
"download": "Download",
|
||||
"executable_path_in_use": "Eksekverbar allerede i brug af \"{{game}}\"",
|
||||
"warning": "Advarsel:",
|
||||
"hydra_needs_to_remain_open": "Hydra skal forblive åbent for at denne download kan gennemføres. I tilfælde af at Hydra lukker før downloaden er færdig, mister du dit fremskridt."
|
||||
},
|
||||
"activation": {
|
||||
"title": "Aktivér Hydra",
|
||||
@@ -81,45 +138,95 @@
|
||||
"enter_activation_code": "Indtast din aktiverings kode",
|
||||
"message": "Hvis du ikke ved hvor du skal spørge om dette, burde du ikke have dette.",
|
||||
"activate": "Aktivér",
|
||||
"loading": "Loader…"
|
||||
"loading": "Indlæser…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Fortsæt",
|
||||
"pause": "Pause",
|
||||
"eta": "Konklusion {{eta}}",
|
||||
"paused": "Pauset",
|
||||
"paused": "Sat på pause",
|
||||
"verifying": "Verificerer…",
|
||||
"completed": "Færdigt",
|
||||
"removed": "Ikke downloadet",
|
||||
"cancel": "Annullér",
|
||||
"filter": "Filtrer downloadet spil",
|
||||
"filter": "Filtrér downloadet spil",
|
||||
"remove": "Fjern",
|
||||
"downloading_metadata": "Downloader metadata…",
|
||||
"deleting": "Sletter installatør…",
|
||||
"delete": "Fjern installatør",
|
||||
"delete_modal_title": "Er du sikker?",
|
||||
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
||||
"install": "Installér"
|
||||
"install": "Installér",
|
||||
"download_in_progress": "Undervejs",
|
||||
"queued_downloads": "Downloadkø",
|
||||
"downloads_completed": "Gennemførte",
|
||||
"queued": "I kø",
|
||||
"no_downloads_title": "Rimelig tomt",
|
||||
"no_downloads_description": "Du har ikke downloadet noget med Hydra endnu, men det er aldrig for sent at begynde.",
|
||||
"checking_files": "Undersøger filer…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads sti",
|
||||
"change": "Opdatering",
|
||||
"change": "Opdatér",
|
||||
"notifications": "Notifikationer",
|
||||
"enable_download_notifications": "Når et download bliver færdigt",
|
||||
"enable_download_notifications": "Når en download bliver færdigt",
|
||||
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
||||
"real_debrid_api_token_label": "Real-Debrid API nøgle",
|
||||
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
|
||||
"launch_with_system": "Åben Hydra ved start af systemet",
|
||||
"general": "Generelt",
|
||||
"behavior": "Opførsel",
|
||||
"download_sources": "Download kilder",
|
||||
"language": "Sprog",
|
||||
"real_debrid_api_token": "API nøgle",
|
||||
"enable_real_debrid": "Slå Real-Debrid til",
|
||||
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
||||
"real_debrid_invalid_token": "Ugyldig API nøgle",
|
||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
||||
"save_changes": "Gem ændringer"
|
||||
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
||||
"real_debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
||||
"save_changes": "Gem ændringer",
|
||||
"changes_saved": "Ændringer gemt successfuldt",
|
||||
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
||||
"validate_download_source": "Validér",
|
||||
"remove_download_source": "Fjern",
|
||||
"add_download_source": "Tilføj kilde",
|
||||
"download_count_zero": "Ingen download muligheder",
|
||||
"download_count_one": "{{countFormatted}} download mulighed",
|
||||
"download_count_other": "{{countFormatted}} download muligheder",
|
||||
"download_source_url": "Download kilde URL",
|
||||
"add_download_source_description": "Indsæt URLen der indeholder .json filen",
|
||||
"download_source_up_to_date": "Op til dato",
|
||||
"download_source_errored": "Fejlede",
|
||||
"sync_download_sources": "Synkronisér kilder",
|
||||
"removed_download_source": "Download kilde fjernet",
|
||||
"added_download_source": "Tilføjede download kilde",
|
||||
"download_sources_synced": "Alle download kilder er synkroniserede",
|
||||
"insert_valid_json_url": "Indsæt en gyldig JSON url",
|
||||
"found_download_option_zero": "Ingen download mulighed fundet",
|
||||
"found_download_option_one": "Fandt {{countFormatted}} download mulighed",
|
||||
"found_download_option_other": "Fandt {{countFormatted}} download mulighed",
|
||||
"import": "Importér",
|
||||
"public": "Offentlig",
|
||||
"private": "Privat",
|
||||
"friends_only": "Kun blandt venner",
|
||||
"privacy": "Privatliv",
|
||||
"profile_visibility": "Synlighed af profil",
|
||||
"profile_visibility_description": "Vælg hvem der kan se din profil og dit bibliotek",
|
||||
"required_field": "Dette felt er påkrævet",
|
||||
"source_already_exists": "Denne kilde er allerede blevet tilføjet",
|
||||
"must_be_valid_url": "Kilden skal være en gyldig URL",
|
||||
"blocked_users": "Blokerede brugere",
|
||||
"user_unblocked": "Brugeren er blevet afblokeret"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download færdig",
|
||||
"game_ready_to_install": "{{title}} er klar til at installeret",
|
||||
"repack_list_updated": "Repack liste opdateret",
|
||||
"repack_count_one": "{{count}} repack tilføjet",
|
||||
"repack_count_other": "{{count}} repacks tilføjet"
|
||||
"repack_count_other": "{{count}} repacks tilføjet",
|
||||
"new_update_available": "Version {{version}} tilgængelig",
|
||||
"restart_to_install_update": "Genstart Hydra for at installere opdateringen"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Åben Hydra",
|
||||
@@ -130,10 +237,80 @@
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programmer ikke installeret",
|
||||
"description": "Wine eller Lutris eksekverbare blev ikke fundet på dit system",
|
||||
"description": "Wine eller Lutris eksekverbar blev ikke fundet på dit system",
|
||||
"instructions": "Tjek den korrekte måde at installere nogle af dem, på din Linux distribution, så spillet kan køre normalt"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Luk knap"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Skift synlighed af kodeord"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} timer",
|
||||
"amount_minutes": "{{amount}} minuter",
|
||||
"last_time_played": "Sidst spillet {{period}}",
|
||||
"activity": "Seneste aktivitet",
|
||||
"library": "Bibliotek",
|
||||
"total_play_time": "Samlet spiltid: {{amount}}",
|
||||
"no_recent_activity_title": "Hmmm… ikke noget her",
|
||||
"no_recent_activity_description": "Du har ikke spillet nogen spil for nyligt. Dét er det på tide at lave om på!",
|
||||
"display_name": "Brugernavn",
|
||||
"saving": "Gemmer",
|
||||
"save": "Gem",
|
||||
"edit_profile": "Redigér Profil",
|
||||
"saved_successfully": "Gemt successfuldt",
|
||||
"try_again": "Venligst, prøv igen",
|
||||
"sign_out_modal_title": "Er du sikker?",
|
||||
"cancel": "Annullér",
|
||||
"successfully_signed_out": "Loggede ud successfuldt",
|
||||
"sign_out": "Log ud",
|
||||
"playing_for": "Spiller i {{amount}}",
|
||||
"sign_out_modal_text": "Dit bibliotek er koblet sammen med din nuværende bruger. Når du logger ud er dit bibliotek ikke synligt længere, og nogen som helst form for fremskridt bliver ikke gemt. Vil du fortsætte med at logge ud?",
|
||||
"add_friends": "Tilføj venner",
|
||||
"add": "Tilføj",
|
||||
"friend_code": "Venne kode",
|
||||
"see_profile": "Se profil",
|
||||
"sending": "Sender",
|
||||
"friend_request_sent": "Venne anmodning sendt",
|
||||
"friends": "Venner",
|
||||
"friends_list": "Venne liste",
|
||||
"user_not_found": "Bruger ikke fundet",
|
||||
"block_user": "Blokér bruger",
|
||||
"add_friend": "Tilføj ven",
|
||||
"request_sent": "Anmodning sendt",
|
||||
"request_received": "Anmodning modtaget",
|
||||
"accept_request": "Acceptér anmodning",
|
||||
"ignore_request": "Ignorér anmodning",
|
||||
"cancel_request": "Annullér anmodning",
|
||||
"undo_friendship": "Fortryd venskab",
|
||||
"request_accepted": "Anmodning accepteret",
|
||||
"user_blocked_successfully": "Bruger blokeret successfuldt",
|
||||
"user_block_modal_text": "Dette blokerer {{displayName}}",
|
||||
"blocked_users": "Blokerede brugere",
|
||||
"unblock": "Afblokér",
|
||||
"no_friends_added": "Du har stadig ikke tilføjet nogen venner",
|
||||
"pending": "Afventer",
|
||||
"no_pending_invites": "Du har ingen afventende invitationer",
|
||||
"no_blocked_users": "Du har ingen blokerede brugere",
|
||||
"friend_code_copied": "Venne kode kopieret",
|
||||
"undo_friendship_modal_text": "Dette vil fortryde dit venskab med {{displayName}}",
|
||||
"privacy_hint": "For at justere hvem der kan se dette, gå til <0>Indstillingerne</0>",
|
||||
"locked_profile": "Denne profil er privat",
|
||||
"image_process_failure": "Fejlede under håndteringen af billedet",
|
||||
"required_field": "Dette felt er påkrævet",
|
||||
"displayname_min_length": "Brugernavnet skal være mindst 3 karakterer langt",
|
||||
"displayname_max_length": "Brugernavnet skal være højest 50 karakterer langt",
|
||||
"report_profile": "Rapportér denne profil",
|
||||
"report_reason": "Hvorfor rapportérer du denne profil?",
|
||||
"report_description": "Yderligere information",
|
||||
"report_description_placeholder": "Yderligere information",
|
||||
"report": "Rapportér",
|
||||
"report_reason_hate": "Hadefuld tale",
|
||||
"report_reason_sexual_content": "Seksuelt indhold",
|
||||
"report_reason_violence": "Vold",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Andet",
|
||||
"profile_reported": "Profil rapporteret"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"surprise_me": "Surprise me",
|
||||
"no_results": "No results found",
|
||||
"start_typing": "Starting typing to search...",
|
||||
"hot": "🔥 Hot now",
|
||||
"hot": "Hot now",
|
||||
"weekly": "📅 Top games of the week"
|
||||
},
|
||||
"sidebar": {
|
||||
@@ -311,6 +311,7 @@
|
||||
"report_reason_violence": "Violence",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Other",
|
||||
"profile_reported": "Profile reported"
|
||||
"profile_reported": "Profile reported",
|
||||
"your_friend_code": "Your friend code:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"trending": "Tendencias",
|
||||
"surprise_me": "¡Sorpréndeme!",
|
||||
"no_results": "No se encontraron resultados",
|
||||
"hot": "🔥 Caliente ahora",
|
||||
"weekly": "📅 Los mejores juegos de la semana",
|
||||
"hot": "Popular Ahora",
|
||||
"weekly": "📅 Destacados de esta semana",
|
||||
"start_typing": "Empieza a escribir para buscar..."
|
||||
},
|
||||
"sidebar": {
|
||||
@@ -123,11 +123,11 @@
|
||||
"download": "Descargar",
|
||||
"download_count": "Descargas",
|
||||
"download_error": "Esta opción de descarga no está disponible.",
|
||||
"executable_path_in_use": "Ejecutable ya en uso por \"{{game}}\"",
|
||||
"nsfw_content_description": "{{title}} incluye contenido que puede no ser adecuado para todas las edades. \n¿Estás seguro de que quieres continuar?",
|
||||
"executable_path_in_use": "El ejecutable se encuentra en uso por \"{{game}}\"",
|
||||
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
||||
"player_count": "Jugadores activos",
|
||||
"refuse_nsfw_content": "Volver",
|
||||
"refuse_nsfw_content": "No, gracias",
|
||||
"stats": "Estadísticas"
|
||||
},
|
||||
"activation": {
|
||||
@@ -209,7 +209,7 @@
|
||||
"download_options_one": "",
|
||||
"download_options_other": "",
|
||||
"download_options_zero": "",
|
||||
"friends_only": "solo amigos",
|
||||
"friends_only": "Solo amigos",
|
||||
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
||||
"privacy": "Privacidad",
|
||||
"private": "Privado",
|
||||
@@ -296,21 +296,21 @@
|
||||
"no_blocked_users": "No has bloqueado a ningún usuario",
|
||||
"friend_code_copied": "Código de amigo copiado",
|
||||
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
||||
"displayname_max_length": "El nombre para mostrar debe tener como máximo 50 caracteres",
|
||||
"displayname_min_length": "El nombre para mostrar debe tener al menos 3 caracteres",
|
||||
"displayname_max_length": "El nombre a mostrar debe tener como máximo 50 caracteres",
|
||||
"displayname_min_length": "El nombre a mostrar debe tener al menos 3 caracteres",
|
||||
"locked_profile": "Este perfil es privado.",
|
||||
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
||||
"profile_locked": "",
|
||||
"profile_locked": "Este perfil es privado",
|
||||
"profile_reported": "Perfil reportado",
|
||||
"report": "Informe",
|
||||
"report": "Reportar",
|
||||
"report_description": "Información adicional",
|
||||
"report_description_placeholder": "Información adicional",
|
||||
"report_profile": "Reportar este perfil",
|
||||
"report_reason": "¿Por qué estás denunciando este perfil?",
|
||||
"report_reason_hate": "Discurso de odio",
|
||||
"report_reason": "¿Cual es el motivo del reporte?",
|
||||
"report_reason_hate": "Discursos de odio",
|
||||
"report_reason_other": "Otro",
|
||||
"report_reason_sexual_content": "Contenido sexual",
|
||||
"report_reason_spam": "Correo basura",
|
||||
"report_reason_spam": "Spam/Contenido no deseado",
|
||||
"report_reason_violence": "Violencia",
|
||||
"required_field": "Este campo es obligatorio",
|
||||
"image_process_failure": "Error al procesar la imagen"
|
||||
|
||||
@@ -22,6 +22,7 @@ import ro from "./ro/translation.json";
|
||||
import ca from "./ca/translation.json";
|
||||
import kk from "./kk/translation.json";
|
||||
import cs from "./cs/translation.json";
|
||||
import nb from "./nb/translation.json";
|
||||
|
||||
export default {
|
||||
"pt-BR": ptBR,
|
||||
@@ -48,4 +49,5 @@ export default {
|
||||
ca,
|
||||
kk,
|
||||
cs,
|
||||
nb,
|
||||
};
|
||||
|
||||
316
src/locales/nb/translation.json
Normal file
316
src/locales/nb/translation.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"language_name": "Norsk Bokmål",
|
||||
"app": {
|
||||
"successfully_signed_in": "Logget inn vellykket"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Anbefalinger",
|
||||
"trending": "Trender",
|
||||
"surprise_me": "Overrask meg",
|
||||
"no_results": "Ingen resultater fundet",
|
||||
"start_typing": "Begynn å skrive for å søke...",
|
||||
"hot": "Populært akkurat nå",
|
||||
"weekly": "📅 De mest populære spillene denne uken"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Nedlastinger",
|
||||
"settings": "Innstillinger",
|
||||
"my_library": "Mitt bibliotek",
|
||||
"downloading_metadata": "{{title}} (Laster ned metadata…)",
|
||||
"paused": "{{title}} (Satt på pause)",
|
||||
"downloading": "{{title}} ({{percentage}} - Laster ned…)",
|
||||
"filter": "Filtrér bibliotek",
|
||||
"home": "Hjem",
|
||||
"queued": "{{title}} (I køen)",
|
||||
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
|
||||
"sign_in": "Logge inn",
|
||||
"friends": "Venner"
|
||||
},
|
||||
"header": {
|
||||
"search": "Søk efter spill",
|
||||
"home": "Hjem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Nedlastinger",
|
||||
"search_results": "Søkeresultater",
|
||||
"settings": "Innstillinger",
|
||||
"version_available_install": "Versjon {{version}} tilgjengelig. Klikk her for å gjenstarte og installere.",
|
||||
"version_available_download": "Versjon {{version}} tilgjengelig. Klikk her for at laste ned."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Ingen nedlastinger pågår",
|
||||
"downloading_metadata": "Laster ned {{title}} metadata…",
|
||||
"downloading": "Laster ned {{title}}… ({{percentage}} ferdig) - Fullstendig nedlastet {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Laster ned {{title}}… ({{percentage}} ferdig) - Regner ut resterende tid…",
|
||||
"checking_files": "Sjekker {{title}} filer… ({{percentage}} ferdig)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Neste side",
|
||||
"previous_page": "Forrige side"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Åpne nedlastingsmuligheter",
|
||||
"download_options_zero": "Ingen nedlastingsmulighet",
|
||||
"download_options_one": "{{count}} nedlastingsmulighet",
|
||||
"download_options_other": "{{count}} nedlastingsmuligheter",
|
||||
"updated_at": "Oppdatert {{updated_at}}",
|
||||
"install": "Installere",
|
||||
"resume": "Fortsett",
|
||||
"pause": "Pause",
|
||||
"cancel": "Kansellere",
|
||||
"remove": "Fjern",
|
||||
"space_left_on_disk": "{{space}} tilbake på harddisken",
|
||||
"eta": "Konklusjon {{eta}}",
|
||||
"calculating_eta": "Utregner resterende tid…",
|
||||
"downloading_metadata": "Laster ned metadata…",
|
||||
"filter": "Filtrér gjennpakkinger",
|
||||
"requirements": "Systemkrav",
|
||||
"minimum": "Mindste",
|
||||
"recommended": "Anbefalet",
|
||||
"paused": "Satt på pause",
|
||||
"release_date": "Offentliggjort den {{date}}",
|
||||
"publisher": "Gitt ut av {{publisher}}",
|
||||
"hours": "timer",
|
||||
"minutes": "minutter",
|
||||
"amount_hours": "{{amount}} timer",
|
||||
"amount_minutes": "{{amount}} minutter",
|
||||
"accuracy": "{{accuracy}}% nøyaktighet",
|
||||
"add_to_library": "Tilføy til biblioteket",
|
||||
"remove_from_library": "Fjern fra biblioteket",
|
||||
"no_downloads": "Ingen nedlastinger tilgjengelig",
|
||||
"play_time": "Spilt i {{amount}}",
|
||||
"last_time_played": "Sist spilt {{period}}",
|
||||
"not_played_yet": "Du har ikke spilt {{title}} enda",
|
||||
"next_suggestion": "Neste forslag",
|
||||
"play": "Spil",
|
||||
"deleting": "Sletter installatør…",
|
||||
"close": "Lukk",
|
||||
"playing_now": "Spiller nå",
|
||||
"change": "Endre",
|
||||
"repacks_modal_description": "Velg den gjennpakking du vil laste ned",
|
||||
"select_folder_hint": "For å endre standard mappen, gå til <0>Innstillingene</0>",
|
||||
"download_now": "Last ned nå",
|
||||
"no_shop_details": "Kunne ikke modta butikksdetaljene.",
|
||||
"download_options": "Nedlastingsmuligheter",
|
||||
"download_path": "Nedlastingssti",
|
||||
"previous_screenshot": "Forrige skjermbilde",
|
||||
"next_screenshot": "Neste skjermbilde",
|
||||
"screenshot": "Skjermbilde {{number}}",
|
||||
"open_screenshot": "Åpen skjermbilde {{number}}",
|
||||
"download_settings": "Nedlastingsinnstillinger",
|
||||
"downloader": "Laster ned",
|
||||
"select_executable": "Velg",
|
||||
"no_executable_selected": "Ingen kjørbar fil valgt",
|
||||
"open_folder": "Åpne mappe",
|
||||
"open_download_location": "Se nedlastingede filer",
|
||||
"create_shortcut": "Opprett snarvei på skrivebordet",
|
||||
"remove_files": "Fjern filer",
|
||||
"remove_from_library_title": "Er du sikker?",
|
||||
"remove_from_library_description": "Dette vil fjerne {{game}} fra biblioteket ditt",
|
||||
"options": "Valgmuligheter",
|
||||
"executable_section_title": "Kjørbar fil",
|
||||
"executable_section_description": "Sti til filen som skal brukes når det trykkes på \"Spill\"",
|
||||
"downloads_secion_title": "Nedlastinger",
|
||||
"downloads_section_description": "Sjekk for oppdateringer eller andre versjoner af dette spillet",
|
||||
"danger_zone_section_title": "Faresonen",
|
||||
"danger_zone_section_description": "Fjern dette spillet fra biblioteket ditt eller filene som har blitt lastet ned av Hydra",
|
||||
"download_in_progress": "Nedlasting pågår",
|
||||
"download_paused": "Nedlasting satt på pause",
|
||||
"last_downloaded_option": "Siste nedlastingsmulighet",
|
||||
"create_shortcut_success": "Opprettelse av snarvei vellykket",
|
||||
"create_shortcut_error": "Feil under oprettelsen av snarvei",
|
||||
"nsfw_content_title": "Dette spillet inneholder upassende innhold",
|
||||
"nsfw_content_description": "{{title}} inneholder innhold som ikke passer til alle aldre. Er du sikker på at du vil fortsette?",
|
||||
"allow_nsfw_content": "Fortsett",
|
||||
"refuse_nsfw_content": "Gå tilbake",
|
||||
"stats": "Statistikk",
|
||||
"download_count": "Nedlastinger",
|
||||
"player_count": "Aktive spillere",
|
||||
"download_error": "Denne nedlastingsmulighet er ikke tilgjengelig",
|
||||
"download": "Last ned",
|
||||
"executable_path_in_use": "Kjørbar fil blir allerede brukt av \"{{game}}\"",
|
||||
"warning": "Advarsel:",
|
||||
"hydra_needs_to_remain_open": "Hydra skal forbli åpent for at denne nedlastingen kan gjennomføres. I tilfelle av at Hydra lukker før nedlastingen er ferdig, mister du fremskrittet ditt."
|
||||
},
|
||||
"activation": {
|
||||
"title": "Aktivér Hydra",
|
||||
"installation_id": "Installasjons ID:",
|
||||
"enter_activation_code": "Inntast aktiveringskoden din",
|
||||
"message": "Hvis du ikke vet hvor du skal spørre om dette, burde du ikke ha dette.",
|
||||
"activate": "Aktivér",
|
||||
"loading": "Innleser…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Fortsett",
|
||||
"pause": "Pause",
|
||||
"eta": "Konklusjon {{eta}}",
|
||||
"paused": "Satt på pause",
|
||||
"verifying": "Verifiserer…",
|
||||
"completed": "Ferdig",
|
||||
"removed": "Ikke lastet ned",
|
||||
"cancel": "Kansellér",
|
||||
"filter": "Filtrér nedlastede spill",
|
||||
"remove": "Fjern",
|
||||
"downloading_metadata": "Laster ned metadata…",
|
||||
"deleting": "Sletter installatør…",
|
||||
"delete": "Fjern installatør",
|
||||
"delete_modal_title": "Er du sikker?",
|
||||
"delete_modal_description": "Dette vil fjerne alle installasjonsfilene fra datamaskinen din",
|
||||
"install": "Installér",
|
||||
"download_in_progress": "Pågår",
|
||||
"queued_downloads": "Nedlastingskø",
|
||||
"downloads_completed": "Gjennomførte",
|
||||
"queued": "I kø",
|
||||
"no_downloads_title": "Ganske tomt",
|
||||
"no_downloads_description": "Du har ikke lastet ned noe med Hydra enda, men det er aldri for sent å begynne.",
|
||||
"checking_files": "Undersøker filer…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Nedlastingssti",
|
||||
"change": "Oppdater",
|
||||
"notifications": "Notifikasjoner",
|
||||
"enable_download_notifications": "Når en nedlasting blir ferdig",
|
||||
"enable_repack_list_notifications": "Når en ny gjennpakking bliver lagt til",
|
||||
"real_debrid_api_token_label": "Real-Debrid API nøkkel",
|
||||
"quit_app_instead_hiding": "Avslut Hydra i stedet for å minimere til prosesslinjen",
|
||||
"launch_with_system": "Åpne Hydra ved oppstart av datamaskinen",
|
||||
"general": "Generelt",
|
||||
"behavior": "Oppførsel",
|
||||
"download_sources": "Nedlastingskilder",
|
||||
"language": "Språk",
|
||||
"real_debrid_api_token": "API nøkkel",
|
||||
"enable_real_debrid": "Slå på Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
||||
"real_debrid_invalid_token": "Ugyldig API nøkkel",
|
||||
"real_debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
||||
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
||||
"real_debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
||||
"save_changes": "Lagre endringer",
|
||||
"changes_saved": "Lagring av endringer vellykket",
|
||||
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
||||
"validate_download_source": "Validér",
|
||||
"remove_download_source": "Fjern",
|
||||
"add_download_source": "Legg til kilde",
|
||||
"download_count_zero": "Ingen nedlastingsmuligheter",
|
||||
"download_count_one": "{{countFormatted}} nedlastingsmulighet",
|
||||
"download_count_other": "{{countFormatted}} nedlastingsmuligheter",
|
||||
"download_source_url": "Last ned kilde URL",
|
||||
"add_download_source_description": "Sett inn URLen som inneholder .json filen",
|
||||
"download_source_up_to_date": "Oppdatert",
|
||||
"download_source_errored": "Mislyktes",
|
||||
"sync_download_sources": "Synkroniser kilder",
|
||||
"removed_download_source": "Nedlastingskilde fjernet",
|
||||
"added_download_source": "La til Nedlastingskilde",
|
||||
"download_sources_synced": "Alle nedlastingskilder er synkroniserte",
|
||||
"insert_valid_json_url": "Innsett en gyldig JSON url",
|
||||
"found_download_option_zero": "Ingen nedlastingsmulighet funnet",
|
||||
"found_download_option_one": "Fant {{countFormatted}} nedlastingsmulighet",
|
||||
"found_download_option_other": "Fant {{countFormatted}} nedlastingsmuligheter",
|
||||
"import": "Importer",
|
||||
"public": "Offentlig",
|
||||
"private": "Privat",
|
||||
"friends_only": "Kun blant venner",
|
||||
"privacy": "Privatliv",
|
||||
"profile_visibility": "Synlighet av profil",
|
||||
"profile_visibility_description": "Velg hvem som kan se profilen din og biblioteket ditt",
|
||||
"required_field": "Dette feltet er påkrevet",
|
||||
"source_already_exists": "Denne kilden har allerede blitt lagt til",
|
||||
"must_be_valid_url": "Kilden må være en gyldig URL",
|
||||
"blocked_users": "Blokerte brukere",
|
||||
"user_unblocked": "Brukeren har blit avblokert"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Nedlasting ferdig",
|
||||
"game_ready_to_install": "{{title}} er klar til å bli installert",
|
||||
"repack_list_updated": "Gjennpakkingslisten er opdateret",
|
||||
"repack_count_one": "{{count}} gjennpakking lagt til",
|
||||
"repack_count_other": "{{count}} gjennpakkinger lagt til",
|
||||
"new_update_available": "Versjon {{version}} tilgjengelig",
|
||||
"restart_to_install_update": "Gjenstart Hydra for å installere oppdateringen"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Åpne Hydra",
|
||||
"quit": "Avslutt"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "Ingen nedlastinger tilgjengelig"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programmer ikke installert",
|
||||
"description": "Wine eller Lutris kjørbar ble ikke funnet på systemet ditt",
|
||||
"instructions": "Sjekk den korrekte måten å installere noen av de, på Linux distributionen din, så spillet kan kjøre på vanlig måte"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Lukk knapp"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Skift synlighet af passord"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} timer",
|
||||
"amount_minutes": "{{amount}} minutter",
|
||||
"last_time_played": "Sist spilt {{period}}",
|
||||
"activity": "Seneste aktivitet",
|
||||
"library": "Bibliotek",
|
||||
"total_play_time": "Samlet spilltid: {{amount}}",
|
||||
"no_recent_activity_title": "Hmmm… ikke noe her",
|
||||
"no_recent_activity_description": "Du har ikke spilt noen spill for på det seneste. Det er det på tide at endre på!",
|
||||
"display_name": "Brukernavn",
|
||||
"saving": "Lagrer",
|
||||
"save": "Lagre",
|
||||
"edit_profile": "Rediger Profil",
|
||||
"saved_successfully": "Lagring vellykket",
|
||||
"try_again": "Vennligst, prøv igjen",
|
||||
"sign_out_modal_title": "Er du sikker?",
|
||||
"cancel": "Kansellér",
|
||||
"successfully_signed_out": "Utlogging vellykket",
|
||||
"sign_out": "Log ut",
|
||||
"playing_for": "Spiller i {{amount}}",
|
||||
"sign_out_modal_text": "Biblioteket ditt er sammenkobelt med den nåverende brukeren. Når du logger ut er biblioteket ditt ikke synlig lenger, og hvilken som helst form for fremskritt bliver ikke lagret. Vil du fortsette med å logge ut?",
|
||||
"add_friends": "Legg til venner",
|
||||
"add": "Legg til",
|
||||
"friend_code": "Vennekode",
|
||||
"see_profile": "Se profil",
|
||||
"sending": "Sender",
|
||||
"friend_request_sent": "Venneforespørsel sendt",
|
||||
"friends": "Venner",
|
||||
"friends_list": "Venneliste",
|
||||
"user_not_found": "Bruker ikke funnet",
|
||||
"block_user": "Blokkere bruker",
|
||||
"add_friend": "Legg til venn",
|
||||
"request_sent": "Forespørsel sendt",
|
||||
"request_received": "Forespørsel modtatt",
|
||||
"accept_request": "Akseptere forespørsel",
|
||||
"ignore_request": "Ignorere forespørsel",
|
||||
"cancel_request": "Kansellre forespørsel",
|
||||
"undo_friendship": "Angre venskab",
|
||||
"request_accepted": "Forespørsel akseptert",
|
||||
"user_blocked_successfully": "Blokkering av bruker vellykket",
|
||||
"user_block_modal_text": "Dette blokerer {{displayName}}",
|
||||
"blocked_users": "Blokerte brukere",
|
||||
"unblock": "Avblokere",
|
||||
"no_friends_added": "Du har fortsatt ikke lagt til noen venner",
|
||||
"pending": "Avventer",
|
||||
"no_pending_invites": "Du har ingen avventende invitasjoner",
|
||||
"no_blocked_users": "Du har ingen blokerte brukere",
|
||||
"friend_code_copied": "Vennekode kopiert",
|
||||
"undo_friendship_modal_text": "Dette vil angre venskapet ditt med {{displayName}}",
|
||||
"privacy_hint": "For å justere på hvem som kan se dette, gå til <0>Innstillingene</0>",
|
||||
"locked_profile": "Denne profilen er privat",
|
||||
"image_process_failure": "Mislyktes under håndteringen av bildet",
|
||||
"required_field": "Dette feltet er påkrevet",
|
||||
"displayname_min_length": "Brukernavnet skal være minst 3 karakterer langt",
|
||||
"displayname_max_length": "Brukernavnet skal være maksimalt 50 karakterer langt",
|
||||
"report_profile": "Rapportér denne profilen",
|
||||
"report_reason": "Hvorfor rapportérer du denne profilen?",
|
||||
"report_description": "Mer informasjon",
|
||||
"report_description_placeholder": "Mer informasjon",
|
||||
"report": "Rapportér",
|
||||
"report_reason_hate": "Hatytringer",
|
||||
"report_reason_sexual_content": "Seksuelt innhold",
|
||||
"report_reason_violence": "Vold",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Annet",
|
||||
"profile_reported": "Profil rapportert"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"home": {
|
||||
"featured": "Destaques",
|
||||
"trending": "Populares",
|
||||
"hot": "🔥 Populares agora",
|
||||
"hot": "Populares agora",
|
||||
"weekly": "📅 Mais baixados da semana",
|
||||
"surprise_me": "Surpreenda-me",
|
||||
"no_results": "Nenhum resultado encontrado",
|
||||
@@ -315,6 +315,7 @@
|
||||
"report_reason_violence": "Violência",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Outro",
|
||||
"profile_reported": "Perfil reportado"
|
||||
"profile_reported": "Perfil reportado",
|
||||
"your_friend_code": "Seu código de amigo:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
"no_pending_invites": "Não tens convites de amizade pendentes",
|
||||
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
|
||||
"friend_code_copied": "Código de amigo copiado",
|
||||
"image_process_failure": "Falha ao processar a imagem"
|
||||
"image_process_failure": "Falha ao processar a imagem",
|
||||
"your_friend_code": "Seu código de amigo:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"trending": "В тренде",
|
||||
"surprise_me": "Удиви меня",
|
||||
"no_results": "Ничего не найдено",
|
||||
"hot": "🔥 Сейчас жарко",
|
||||
"hot": "Сейчас жарко",
|
||||
"start_typing": "Начинаю вводить текст для поиска...",
|
||||
"weekly": "📅 Лучшие игры недели"
|
||||
},
|
||||
|
||||
@@ -11,3 +11,5 @@ export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
||||
export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
: path.join(__dirname, "..", "..", "seeds");
|
||||
|
||||
export const appVersion = app.getVersion();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Steam250Game>(), 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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -1,7 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { fetchDownloadSourcesAndUpdate } from "@main/helpers";
|
||||
|
||||
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
fetchDownloadSourcesAndUpdate();
|
||||
|
||||
registerEvent("syncDownloadSources", syncDownloadSources);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultDownloadsPath } from "@main/constants";
|
||||
import { app, ipcMain } from "electron";
|
||||
import { appVersion, defaultDownloadsPath } from "@main/constants";
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import "./catalogue/get-catalogue";
|
||||
import "./catalogue/get-game-shop-details";
|
||||
@@ -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 "./hardware/get-disk-free-space";
|
||||
@@ -37,11 +36,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";
|
||||
@@ -59,9 +55,11 @@ import "./profile/update-friend-request";
|
||||
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");
|
||||
ipcMain.handle("getVersion", () => app.getVersion());
|
||||
ipcMain.handle("getVersion", () => appVersion);
|
||||
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||
|
||||
@@ -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";
|
||||
@@ -36,20 +35,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,
|
||||
});
|
||||
}
|
||||
|
||||
const game = await gameRepository.findOne({ where: { objectID } });
|
||||
|
||||
@@ -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);
|
||||
@@ -1,32 +1,14 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import { UserProfile } from "@types";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { ProfileVisibility, UserDetails } from "@types";
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { steamUrlBuilder, UserNotLoggedInError } from "@shared";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const getSteamGame = async (objectId: string) => {
|
||||
try {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
return {
|
||||
title: steamGame.name,
|
||||
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error("Failed to get Steam game", err);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
|
||||
const getMe = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
): Promise<UserProfile | null> => {
|
||||
return HydraApi.get(`/profile/me`)
|
||||
): Promise<UserDetails | null> => {
|
||||
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||
.then(async (me) => {
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
@@ -38,17 +20,6 @@ const getMe = async (
|
||||
["id"]
|
||||
);
|
||||
|
||||
if (me.currentGame) {
|
||||
const steamGame = await getSteamGame(me.currentGame.objectId);
|
||||
|
||||
if (steamGame) {
|
||||
me.currentGame = {
|
||||
...me.currentGame,
|
||||
...steamGame,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Sentry.setUser({ id: me.id, username: me.username });
|
||||
|
||||
return me;
|
||||
@@ -61,7 +32,13 @@ const getMe = async (
|
||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
|
||||
if (loggedUser) {
|
||||
return { ...loggedUser, id: loggedUser.userId };
|
||||
return {
|
||||
...loggedUser,
|
||||
id: loggedUser.userId,
|
||||
username: "",
|
||||
bio: "",
|
||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
9
src/main/events/profile/sync-friend-requests.ts
Normal file
9
src/main/events/profile/sync-friend-requests.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { FriendRequestSync } from "@types";
|
||||
|
||||
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`);
|
||||
};
|
||||
|
||||
registerEvent("syncFriendRequests", syncFriendRequests);
|
||||
@@ -1,44 +1,32 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { getFileBase64 } from "@main/helpers";
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||
|
||||
import { Not } from "typeorm";
|
||||
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({
|
||||
@@ -101,6 +79,17 @@ const startGameDownload = async (
|
||||
|
||||
createGame(updatedGame!).catch(() => {});
|
||||
|
||||
HydraApi.post(
|
||||
"/games/download",
|
||||
{
|
||||
objectId: updatedGame!.objectID,
|
||||
shop: updatedGame!.shop,
|
||||
},
|
||||
{ needsAuth: false }
|
||||
).catch((err) => {
|
||||
logger.error("Failed to create game download", err);
|
||||
});
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ const getUser = async (
|
||||
recentGames,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadSource, Repack } from "@main/entity";
|
||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { RepacksManager } from "@main/services";
|
||||
import { downloadSourceWorker } from "@main/workers";
|
||||
import { chunk } from "lodash-es";
|
||||
import type { EntityManager } from "typeorm";
|
||||
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import { z } from "zod";
|
||||
|
||||
export const insertDownloadsFromSource = async (
|
||||
trx: EntityManager,
|
||||
downloadSource: DownloadSource,
|
||||
downloads: z.infer<typeof downloadSourceSchema>["downloads"]
|
||||
) => {
|
||||
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
||||
(download) => ({
|
||||
title: download.title,
|
||||
uris: JSON.stringify(download.uris),
|
||||
magnet: download.uris[0]!,
|
||||
fileSize: download.fileSize,
|
||||
repacker: downloadSource.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSource: { id: downloadSource.id },
|
||||
})
|
||||
);
|
||||
|
||||
const downloadsChunks = chunk(repacks, 800);
|
||||
|
||||
for (const chunk of downloadsChunks) {
|
||||
await trx
|
||||
.getRepository(Repack)
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(chunk)
|
||||
.updateEntity(false)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDownloadSourcesAndUpdate = async () => {
|
||||
const downloadSources = await downloadSourceRepository.find({
|
||||
order: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const results = await downloadSourceWorker.run(downloadSources, {
|
||||
name: "getUpdatedRepacks",
|
||||
});
|
||||
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
for (const result of results) {
|
||||
if (result.etag !== null) {
|
||||
await transactionalEntityManager.getRepository(DownloadSource).update(
|
||||
{ id: result.id },
|
||||
{
|
||||
etag: result.etag,
|
||||
status: result.status,
|
||||
downloadCount: result.downloads.length,
|
||||
}
|
||||
);
|
||||
|
||||
await insertDownloadsFromSource(
|
||||
transactionalEntityManager,
|
||||
result,
|
||||
result.downloads
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await RepacksManager.updateRepacks();
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -104,6 +103,46 @@ app.whenReady().then(async () => {
|
||||
|
||||
WindowManager.createMainWindow();
|
||||
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) => {
|
||||
|
||||
@@ -3,12 +3,21 @@ import { databasePath } from "./constants";
|
||||
import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
|
||||
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
||||
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
||||
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
||||
import { app } from "electron";
|
||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
getMigrations(): Promise<HydraMigration[]> {
|
||||
return Promise.resolve([Hydra2_0_3, RepackUris, UpdateUserLanguage]);
|
||||
return Promise.resolve([
|
||||
Hydra2_0_3,
|
||||
RepackUris,
|
||||
UpdateUserLanguage,
|
||||
EnsureRepackUris,
|
||||
FixMissingColumns,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
return migration.name;
|
||||
@@ -19,6 +28,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
}
|
||||
|
||||
export const knexClient = knex({
|
||||
debug: !app.isPackaged,
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: databasePath,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,55 +4,15 @@ import type { Knex } from "knex";
|
||||
export const RepackUris: HydraMigration = {
|
||||
name: "RepackUris",
|
||||
up: async (knex: Knex) => {
|
||||
await knex.schema.createTable("temporary_repack", (table) => {
|
||||
const timestamp = new Date().getTime();
|
||||
table.increments("id").primary();
|
||||
table
|
||||
.text("title")
|
||||
.notNullable()
|
||||
.unique({ indexName: "repack_title_unique_" + timestamp });
|
||||
table
|
||||
.text("magnet")
|
||||
.notNullable()
|
||||
.unique({ indexName: "repack_magnet_unique_" + timestamp });
|
||||
table.text("repacker").notNullable();
|
||||
table.text("fileSize").notNullable();
|
||||
table.datetime("uploadDate").notNullable();
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
table
|
||||
.integer("downloadSourceId")
|
||||
.references("download_source.id")
|
||||
.onDelete("CASCADE");
|
||||
await knex.schema.alterTable("repack", (table) => {
|
||||
table.text("uris").notNullable().defaultTo("[]");
|
||||
});
|
||||
await knex.raw(
|
||||
`INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`
|
||||
);
|
||||
await knex.schema.dropTable("repack");
|
||||
await knex.schema.renameTable("temporary_repack", "repack");
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
await knex.schema.renameTable("repack", "temporary_repack");
|
||||
await knex.schema.createTable("repack", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("title").notNullable().unique();
|
||||
table.text("magnet").notNullable().unique();
|
||||
await knex.schema.alterTable("repack", (table) => {
|
||||
table.integer("page");
|
||||
table.text("repacker").notNullable();
|
||||
table.text("fileSize").notNullable();
|
||||
table.datetime("uploadDate").notNullable();
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
table
|
||||
.integer("downloadSourceId")
|
||||
.references("download_source.id")
|
||||
.onDelete("CASCADE");
|
||||
table.dropColumn("uris");
|
||||
});
|
||||
await knex.raw(
|
||||
`INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`
|
||||
);
|
||||
await knex.schema.dropTable("temporary_repack");
|
||||
},
|
||||
};
|
||||
|
||||
17
src/main/migrations/20240915035339_ensure_repack_uris.ts
Normal file
17
src/main/migrations/20240915035339_ensure_repack_uris.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const EnsureRepackUris: HydraMigration = {
|
||||
name: "EnsureRepackUris",
|
||||
up: async (knex: Knex) => {
|
||||
await knex.schema.hasColumn("repack", "uris").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("repack", (table) => {
|
||||
table.text("uris").notNullable().defaultTo("[]");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: async (_knex: Knex) => {},
|
||||
};
|
||||
41
src/main/migrations/20240918001920_FixMissingColumns.ts
Normal file
41
src/main/migrations/20240918001920_FixMissingColumns.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const FixMissingColumns: HydraMigration = {
|
||||
name: "FixMissingColumns",
|
||||
up: async (knex: Knex) => {
|
||||
const timestamp = new Date().getTime();
|
||||
await knex.schema
|
||||
.hasColumn("repack", "downloadSourceId")
|
||||
.then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("repack", (table) => {
|
||||
table
|
||||
.integer("downloadSourceId")
|
||||
.references("download_source.id")
|
||||
.onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasColumn("game", "remoteId").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("game", (table) => {
|
||||
table
|
||||
.text("remoteId")
|
||||
.unique({ indexName: "game_remoteId_unique_" + timestamp });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.hasColumn("game", "uri").then(async (exists) => {
|
||||
if (!exists) {
|
||||
await knex.schema.table("game", (table) => {
|
||||
table.text("uri");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: async (_knex: Knex) => {},
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import { uploadGamesBatch } from "./library-sync";
|
||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||
import { logger } from "./logger";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { omit } from "lodash-es";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
interface HydraApiOptions {
|
||||
needsAuth: boolean;
|
||||
@@ -49,7 +51,10 @@ export class HydraApi {
|
||||
expirationTimestamp: tokenExpirationTimestamp,
|
||||
};
|
||||
|
||||
logger.log("Sign in received", this.userAuth);
|
||||
logger.log(
|
||||
"Sign in received. Token expiration timestamp:",
|
||||
tokenExpirationTimestamp
|
||||
);
|
||||
|
||||
await userAuthRepository.upsert(
|
||||
{
|
||||
@@ -79,12 +84,16 @@ export class HydraApi {
|
||||
static async setupApi() {
|
||||
this.instance = axios.create({
|
||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
||||
});
|
||||
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
logger.log(request.method, request.url, request.params, request.data);
|
||||
const data = Array.isArray(request.data)
|
||||
? request.data
|
||||
: omit(request.data, ["refreshToken"]);
|
||||
logger.log(request.method, request.url, request.params, data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
@@ -96,11 +105,14 @@ export class HydraApi {
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
logger.log(" ---- RESPONSE -----");
|
||||
const data = Array.isArray(response.data)
|
||||
? response.data
|
||||
: omit(response.data, ["username", "accessToken", "refreshToken"]);
|
||||
logger.log(
|
||||
response.status,
|
||||
response.config.method,
|
||||
response.config.url,
|
||||
response.data
|
||||
data
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@@ -166,7 +178,10 @@ export class HydraApi {
|
||||
this.userAuth.authToken = accessToken;
|
||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||
|
||||
logger.log("Token refreshed", this.userAuth);
|
||||
logger.log(
|
||||
"Token refreshed. New expiration:",
|
||||
this.userAuth.expirationTimestamp
|
||||
);
|
||||
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { Game } from "@main/entity";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export const createGame = async (game: Game) => {
|
||||
HydraApi.post(
|
||||
"/games/download",
|
||||
{
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
},
|
||||
{ needsAuth: false }
|
||||
).catch((err) => {
|
||||
logger.error("Failed to create game download", err);
|
||||
});
|
||||
|
||||
return HydraApi.post(`/profile/games`, {
|
||||
objectId: game.objectID,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
|
||||
@@ -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
|
||||
) => {
|
||||
|
||||
@@ -119,7 +119,7 @@ const onCloseGame = (game: Game) => {
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
performance.now() - gamePlaytime.firstTick,
|
||||
performance.now() - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
).catch(() => {});
|
||||
} else {
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export class WindowManager {
|
||||
});
|
||||
|
||||
authWindow.loadURL(
|
||||
`https://auth.hydralauncher.gg/?${searchParams.toString()}`
|
||||
`${import.meta.env.MAIN_VITE_AUTH_URL}/?${searchParams.toString()}`
|
||||
);
|
||||
|
||||
authWindow.once("ready-to-show", () => {
|
||||
|
||||
1
src/main/vite-env.d.ts
vendored
1
src/main/vite-env.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
||||
readonly MAIN_VITE_API_URL: string;
|
||||
readonly MAIN_VITE_AUTH_URL: string;
|
||||
readonly MAIN_VITE_SENTRY_DSN: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<typeof downloadSourceSchema> & {
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -60,13 +60,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) =>
|
||||
@@ -150,6 +145,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
processProfileImage: (imagePath: string) =>
|
||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||
sendFriendRequest: (userId: string) =>
|
||||
@@ -181,4 +177,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.on("on-signout", listener);
|
||||
return () => ipcRenderer.removeListener("on-signout", listener);
|
||||
},
|
||||
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Hydra</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com https://video.akamai.steamstatic.com;"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
|
||||
/>
|
||||
</head>
|
||||
<body style="background-color: #1c1c1c">
|
||||
|
||||
@@ -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,13 +39,17 @@ export function App() {
|
||||
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const downloadSourceMigrationLock = useRef(false);
|
||||
|
||||
const { clearDownload, setLastPacket } = useDownload();
|
||||
|
||||
const { indexRepacks } = useContext(repacksContext);
|
||||
|
||||
const {
|
||||
isFriendsModalVisible,
|
||||
friendRequetsModalTab,
|
||||
friendModalUserId,
|
||||
fetchFriendRequests,
|
||||
syncFriendRequests,
|
||||
hideFriendsModal,
|
||||
} = useUserDetails();
|
||||
|
||||
@@ -105,22 +111,22 @@ export function App() {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
fetchFriendRequests();
|
||||
syncFriendRequests();
|
||||
}
|
||||
});
|
||||
}, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
|
||||
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
||||
|
||||
const onSignIn = useCallback(() => {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
fetchFriendRequests();
|
||||
syncFriendRequests();
|
||||
showSuccessToast(t("successfully_signed_in"));
|
||||
}
|
||||
});
|
||||
}, [
|
||||
fetchUserDetails,
|
||||
fetchFriendRequests,
|
||||
syncFriendRequests,
|
||||
t,
|
||||
showSuccessToast,
|
||||
updateUserDetails,
|
||||
@@ -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<number>) => {
|
||||
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]);
|
||||
|
||||
1735
src/renderer/src/assets/lottie/flame.json
Normal file
1735
src/renderer/src/assets/lottie/flame.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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<GameStats | null>(null);
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useRef } from "react";
|
||||
import Lottie from "lottie-react";
|
||||
|
||||
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
|
||||
@@ -8,11 +7,8 @@ export interface DownloadIconProps {
|
||||
}
|
||||
|
||||
export function DownloadIcon({ isDownloading }: DownloadIconProps) {
|
||||
const lottieRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Lottie
|
||||
lottieRef={lottieRef}
|
||||
animationData={downloadingAnimation}
|
||||
loop={isDownloading}
|
||||
autoplay={isDownloading}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 60_000;
|
||||
|
||||
@@ -15,15 +16,15 @@ export function SidebarProfile() {
|
||||
|
||||
const { t } = useTranslation("sidebar");
|
||||
|
||||
const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } =
|
||||
useUserDetails();
|
||||
const {
|
||||
userDetails,
|
||||
friendRequestCount,
|
||||
showFriendsModal,
|
||||
syncFriendRequests,
|
||||
} = useUserDetails();
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const receivedRequests = useMemo(() => {
|
||||
return friendRequests.filter((request) => request.type === "RECEIVED");
|
||||
}, [friendRequests]);
|
||||
|
||||
const handleProfileClick = () => {
|
||||
if (userDetails === null) {
|
||||
window.electron.openAuthWindow();
|
||||
@@ -35,7 +36,7 @@ export function SidebarProfile() {
|
||||
|
||||
useEffect(() => {
|
||||
pollingInterval.current = setInterval(() => {
|
||||
fetchFriendRequests();
|
||||
syncFriendRequests();
|
||||
}, LONG_POLLING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
@@ -43,7 +44,7 @@ export function SidebarProfile() {
|
||||
clearInterval(pollingInterval.current);
|
||||
}
|
||||
};
|
||||
}, [fetchFriendRequests]);
|
||||
}, [syncFriendRequests]);
|
||||
|
||||
const friendsButton = useMemo(() => {
|
||||
if (!userDetails) return null;
|
||||
@@ -57,16 +58,33 @@ export function SidebarProfile() {
|
||||
}
|
||||
title={t("friends")}
|
||||
>
|
||||
{receivedRequests.length > 0 && (
|
||||
{friendRequestCount > 0 && (
|
||||
<small className={styles.friendsButtonBadge}>
|
||||
{receivedRequests.length > 99 ? "99+" : receivedRequests.length}
|
||||
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||
</small>
|
||||
)}
|
||||
|
||||
<PeopleIcon size={16} />
|
||||
</button>
|
||||
);
|
||||
}, [userDetails, t, receivedRequests, showFriendsModal]);
|
||||
}, [userDetails, t, friendRequestCount, showFriendsModal]);
|
||||
|
||||
const gameRunningDetails = () => {
|
||||
if (!userDetails || !gameRunning) return null;
|
||||
|
||||
if (gameRunning.iconUrl) {
|
||||
return (
|
||||
<img
|
||||
alt={gameRunning.title}
|
||||
width={24}
|
||||
style={{ borderRadius: 4 }}
|
||||
src={gameRunning.iconUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SteamLogo />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.profileContainer}>
|
||||
@@ -100,6 +118,7 @@ export function SidebarProfile() {
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<small>{gameRunning.title}</small>
|
||||
@@ -107,14 +126,7 @@ export function SidebarProfile() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userDetails && gameRunning && (
|
||||
<img
|
||||
alt={gameRunning.title}
|
||||
width={24}
|
||||
style={{ borderRadius: 4 }}
|
||||
src={gameRunning.iconUrl!}
|
||||
/>
|
||||
)}
|
||||
{gameRunningDetails()}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const sidebar = recipe({
|
||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
||||
},
|
||||
false: {
|
||||
paddingTop: `${SPACING_UNIT * 2}px`,
|
||||
paddingTop: `${SPACING_UNIT}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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<GameDetailsContext>({
|
||||
game: null,
|
||||
@@ -52,7 +59,6 @@ export function GameDetailsContextProvider({
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
const [game, setGame] = useState<Game | null>(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<GameRepack[]>([]);
|
||||
|
||||
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]);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
67
src/renderer/src/context/repacks/repacks.context.tsx
Normal file
67
src/renderer/src/context/repacks/repacks.context.tsx
Normal file
@@ -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<GameRepack[]>;
|
||||
indexRepacks: () => void;
|
||||
isIndexingRepacks: boolean;
|
||||
}
|
||||
|
||||
export const repacksContext = createContext<RepacksContext>({
|
||||
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<GameRepack[]>((resolve) => {
|
||||
const channelId = crypto.randomUUID();
|
||||
repacksWorker.postMessage([channelId, query]);
|
||||
|
||||
const channel = new BroadcastChannel(`repacks:search:${channelId}`);
|
||||
channel.onmessage = (event: MessageEvent<GameRepack[]>) => {
|
||||
resolve(event.data);
|
||||
channel.close();
|
||||
};
|
||||
|
||||
return [];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const indexRepacks = useCallback(() => {
|
||||
setIsIndexingRepacks(true);
|
||||
repacksWorker.postMessage("INDEX_REPACKS");
|
||||
|
||||
repacksWorker.onmessage = () => {
|
||||
setIsIndexingRepacks(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
indexRepacks();
|
||||
}, [indexRepacks]);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
searchRepacks,
|
||||
indexRepacks,
|
||||
isIndexingRepacks,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
15
src/renderer/src/declaration.d.ts
vendored
15
src/renderer/src/declaration.d.ts
vendored
@@ -23,6 +23,8 @@ import type {
|
||||
GameStats,
|
||||
TrendingGame,
|
||||
UserStats,
|
||||
UserDetails,
|
||||
FriendRequestSync,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@@ -100,12 +102,7 @@ declare global {
|
||||
|
||||
/* Download sources */
|
||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||
validateDownloadSource: (
|
||||
url: string
|
||||
) => Promise<{ name: string; downloadCount: number }>;
|
||||
addDownloadSource: (url: string) => Promise<DownloadSource>;
|
||||
removeDownloadSource: (id: number) => Promise<void>;
|
||||
syncDownloadSources: () => Promise<void>;
|
||||
deleteDownloadSource: (id: number) => Promise<void>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
@@ -153,7 +150,7 @@ declare global {
|
||||
) => Promise<void>;
|
||||
|
||||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
getMe: () => Promise<UserDetails | null>;
|
||||
undoFriendship: (userId: string) => Promise<void>;
|
||||
updateProfile: (
|
||||
updateProfile: UpdateProfileRequest
|
||||
@@ -163,11 +160,15 @@ declare global {
|
||||
path: string
|
||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||
syncFriendRequests: () => Promise<FriendRequestSync>;
|
||||
updateFriendRequest: (
|
||||
userId: string,
|
||||
action: FriendRequestAction
|
||||
) => Promise<void>;
|
||||
sendFriendRequest: (userId: string) => Promise<void>;
|
||||
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
||||
13
src/renderer/src/dexie.ts
Normal file
13
src/renderer/src/dexie.ts
Normal file
@@ -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();
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import type { FriendRequest, UserProfile } from "@types";
|
||||
import type { FriendRequest, UserDetails } from "@types";
|
||||
|
||||
export interface UserDetailsState {
|
||||
userDetails: UserProfile | null;
|
||||
userDetails: UserDetails | null;
|
||||
profileBackground: null | string;
|
||||
friendRequests: FriendRequest[];
|
||||
friendRequestCount: number;
|
||||
isFriendsModalVisible: boolean;
|
||||
friendRequetsModalTab: UserFriendModalTab | null;
|
||||
friendModalUserId: string;
|
||||
@@ -15,6 +16,7 @@ const initialState: UserDetailsState = {
|
||||
userDetails: null,
|
||||
profileBackground: null,
|
||||
friendRequests: [],
|
||||
friendRequestCount: 0,
|
||||
isFriendsModalVisible: false,
|
||||
friendRequetsModalTab: null,
|
||||
friendModalUserId: "",
|
||||
@@ -24,7 +26,7 @@ export const userDetailsSlice = createSlice({
|
||||
name: "user-details",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUserDetails: (state, action: PayloadAction<UserProfile | null>) => {
|
||||
setUserDetails: (state, action: PayloadAction<UserDetails | null>) => {
|
||||
state.userDetails = action.payload;
|
||||
},
|
||||
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||
@@ -33,6 +35,9 @@ export const userDetailsSlice = createSlice({
|
||||
setFriendRequests: (state, action: PayloadAction<FriendRequest[]>) => {
|
||||
state.friendRequests = action.payload;
|
||||
},
|
||||
setFriendRequestCount: (state, action: PayloadAction<number>) => {
|
||||
state.friendRequestCount = action.payload;
|
||||
},
|
||||
setFriendsModalVisible: (
|
||||
state,
|
||||
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
||||
@@ -52,6 +57,7 @@ export const {
|
||||
setUserDetails,
|
||||
setProfileBackground,
|
||||
setFriendRequests,
|
||||
setFriendRequestCount,
|
||||
setFriendsModalVisible,
|
||||
setFriendsModalHidden,
|
||||
} = userDetailsSlice.actions;
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
setFriendRequests,
|
||||
setFriendsModalVisible,
|
||||
setFriendsModalHidden,
|
||||
setFriendRequestCount,
|
||||
} from "@renderer/features";
|
||||
import type {
|
||||
FriendRequestAction,
|
||||
UpdateProfileRequest,
|
||||
UserProfile,
|
||||
UserDetails,
|
||||
} from "@types";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
|
||||
@@ -21,6 +22,7 @@ export function useUserDetails() {
|
||||
userDetails,
|
||||
profileBackground,
|
||||
friendRequests,
|
||||
friendRequestCount,
|
||||
isFriendsModalVisible,
|
||||
friendModalUserId,
|
||||
friendRequetsModalTab,
|
||||
@@ -40,7 +42,7 @@ export function useUserDetails() {
|
||||
}, [clearUserDetails]);
|
||||
|
||||
const updateUserDetails = useCallback(
|
||||
async (userDetails: UserProfile) => {
|
||||
async (userDetails: UserDetails) => {
|
||||
dispatch(setUserDetails(userDetails));
|
||||
|
||||
if (userDetails.profileImageUrl) {
|
||||
@@ -83,19 +85,32 @@ export function useUserDetails() {
|
||||
const patchUser = useCallback(
|
||||
async (values: UpdateProfileRequest) => {
|
||||
const response = await window.electron.updateProfile(values);
|
||||
return updateUserDetails(response);
|
||||
return updateUserDetails({
|
||||
...response,
|
||||
username: userDetails?.username || "",
|
||||
});
|
||||
},
|
||||
[updateUserDetails]
|
||||
[updateUserDetails, userDetails?.username]
|
||||
);
|
||||
|
||||
const syncFriendRequests = useCallback(async () => {
|
||||
return window.electron
|
||||
.syncFriendRequests()
|
||||
.then((sync) => {
|
||||
dispatch(setFriendRequestCount(sync.friendRequestCount));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [dispatch]);
|
||||
|
||||
const fetchFriendRequests = useCallback(async () => {
|
||||
return window.electron
|
||||
.getFriendRequests()
|
||||
.then((friendRequests) => {
|
||||
syncFriendRequests();
|
||||
dispatch(setFriendRequests(friendRequests));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [dispatch]);
|
||||
}, [dispatch, syncFriendRequests]);
|
||||
|
||||
const showFriendsModal = useCallback(
|
||||
(initialTab: UserFriendModalTab, userId: string) => {
|
||||
@@ -140,6 +155,7 @@ export function useUserDetails() {
|
||||
userDetails,
|
||||
profileBackground,
|
||||
friendRequests,
|
||||
friendRequestCount,
|
||||
friendRequetsModalTab,
|
||||
isFriendsModalVisible,
|
||||
friendModalUserId,
|
||||
@@ -152,6 +168,7 @@ export function useUserDetails() {
|
||||
patchUser,
|
||||
sendFriendRequest,
|
||||
fetchFriendRequests,
|
||||
syncFriendRequests,
|
||||
updateFriendRequestState,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
|
||||
@@ -29,6 +29,9 @@ import { store } from "./store";
|
||||
|
||||
import resources from "@locales";
|
||||
|
||||
import "./workers";
|
||||
import { RepacksContextProvider } from "./context";
|
||||
|
||||
Sentry.init({});
|
||||
|
||||
i18n
|
||||
@@ -54,19 +57,21 @@ i18n
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route element={<App />}>
|
||||
<Route path="/" Component={Home} />
|
||||
<Route path="/catalogue" Component={Catalogue} />
|
||||
<Route path="/downloads" Component={Downloads} />
|
||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
<RepacksContextProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route element={<App />}>
|
||||
<Route path="/" Component={Home} />
|
||||
<Route path="/catalogue" Component={Catalogue} />
|
||||
<Route path="/downloads" Component={Downloads} />
|
||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</RepacksContextProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ export const gallerySliderMedia = style({
|
||||
flexGrow: "0",
|
||||
transition: "translate 0.3s ease-in-out",
|
||||
borderRadius: "4px",
|
||||
alignSelf: "center",
|
||||
});
|
||||
|
||||
export const gallerySliderAnimationContainer = style({
|
||||
@@ -60,7 +61,6 @@ export const mediaPreviewButton = recipe({
|
||||
base: {
|
||||
cursor: "pointer",
|
||||
width: "20%",
|
||||
height: "20%",
|
||||
display: "block",
|
||||
flexShrink: "0",
|
||||
flexGrow: "0",
|
||||
@@ -84,7 +84,6 @@ export const mediaPreviewButton = recipe({
|
||||
|
||||
export const mediaPreview = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ export function GallerySlider() {
|
||||
src={image.path_full}
|
||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||
alt={t("screenshot", { number: i + 1 })}
|
||||
loading="lazy"
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -149,8 +149,7 @@ export const randomizerButton = style({
|
||||
animationName: slideIn,
|
||||
animationDuration: "0.2s",
|
||||
position: "fixed",
|
||||
/* Bottom panel height + spacing */
|
||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
||||
bottom: `${SPACING_UNIT * 3}px`,
|
||||
/* Scroll bar + spacing */
|
||||
right: `${9 + SPACING_UNIT * 2}px`,
|
||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
||||
|
||||
@@ -160,12 +160,15 @@ export function DownloadSettingsModal({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedDownloader && selectedDownloader !== Downloader.Torrent && (
|
||||
<p style={{ marginTop: `${SPACING_UNIT}px` }}>
|
||||
<span style={{ color: vars.color.warning }}>{t("warning")}</span>{" "}
|
||||
{t("hydra_needs_to_remain_open")}
|
||||
</p>
|
||||
)}
|
||||
{selectedDownloader != null &&
|
||||
selectedDownloader !== Downloader.Torrent && (
|
||||
<p style={{ marginTop: `${SPACING_UNIT}px` }}>
|
||||
<span style={{ color: vars.color.warning }}>
|
||||
{t("warning")}
|
||||
</span>{" "}
|
||||
{t("hydra_needs_to_remain_open")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -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<GameRepack | null>(null);
|
||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||
|
||||
const [infoHash, setInfoHash] = useState<string | null>(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 ?? ""));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@renderer/components";
|
||||
@@ -9,7 +9,7 @@ import { useFormat } from "@renderer/hooks";
|
||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||
|
||||
export function Sidebar() {
|
||||
const [_howLongToBeat, setHowLongToBeat] = useState<{
|
||||
const [_howLongToBeat, _setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
data: HowLongToBeatCategory[] | null;
|
||||
}>({ isLoading: true, data: null });
|
||||
@@ -17,27 +17,26 @@ export function Sidebar() {
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const { gameTitle, shopDetails, objectID, stats } =
|
||||
useContext(gameDetailsContext);
|
||||
const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
useEffect(() => {
|
||||
if (objectID) {
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
// useEffect(() => {
|
||||
// if (objectID) {
|
||||
// setHowLongToBeat({ isLoading: true, data: null });
|
||||
|
||||
window.electron
|
||||
.getHowLongToBeat(objectID, "steam", gameTitle)
|
||||
.then((howLongToBeat) => {
|
||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
})
|
||||
.catch(() => {
|
||||
setHowLongToBeat({ isLoading: false, data: null });
|
||||
});
|
||||
}
|
||||
}, [objectID, gameTitle]);
|
||||
// window.electron
|
||||
// .getHowLongToBeat(objectID, "steam", gameTitle)
|
||||
// .then((howLongToBeat) => {
|
||||
// setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
// })
|
||||
// .catch(() => {
|
||||
// setHowLongToBeat({ isLoading: false, data: null });
|
||||
// });
|
||||
// }
|
||||
// }, [objectID, gameTitle]);
|
||||
|
||||
return (
|
||||
<aside className={styles.contentSidebar}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -8,10 +8,11 @@ import { Button, GameCard, Hero } from "@renderer/components";
|
||||
import type { Steam250Game, CatalogueEntry } from "@types";
|
||||
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
import flameAnimation from "@renderer/assets/lottie/flame.json";
|
||||
|
||||
import * as styles from "./home.css";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import Lottie from "lottie-react";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { CatalogueCategory } from "@shared";
|
||||
|
||||
@@ -19,6 +20,8 @@ export function Home() {
|
||||
const { t } = useTranslation("home");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const flameAnimationRef = useRef<LottieRefCurrentProps>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
|
||||
@@ -82,6 +85,18 @@ export function Home() {
|
||||
|
||||
const categories = Object.values(CatalogueCategory);
|
||||
|
||||
const handleMouseEnterCategory = (category: CatalogueCategory) => {
|
||||
if (category === CatalogueCategory.Hot) {
|
||||
flameAnimationRef?.current?.play();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeaveCategory = (category: CatalogueCategory) => {
|
||||
if (category === CatalogueCategory.Hot) {
|
||||
flameAnimationRef?.current?.stop();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<section className={styles.content}>
|
||||
@@ -100,7 +115,28 @@ export function Home() {
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
onMouseEnter={() => handleMouseEnterCategory(category)}
|
||||
onMouseLeave={() => handleMouseLeaveCategory(category)}
|
||||
>
|
||||
{category === CatalogueCategory.Hot && (
|
||||
<div
|
||||
style={{ width: 16, height: 16, position: "relative" }}
|
||||
>
|
||||
<Lottie
|
||||
lottieRef={flameAnimationRef}
|
||||
animationData={flameAnimation}
|
||||
loop
|
||||
autoplay={false}
|
||||
style={{
|
||||
width: 30,
|
||||
top: -10,
|
||||
left: -5,
|
||||
position: "absolute",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{t(category)}
|
||||
</Button>
|
||||
</li>
|
||||
@@ -116,14 +152,32 @@ export function Home() {
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
||||
loop
|
||||
loop={Boolean(randomGame)}
|
||||
/>
|
||||
</div>
|
||||
{t("surprise_me")}
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<h2>{t(currentCatalogueCategory)}</h2>
|
||||
<h2 style={{ display: "flex", gap: SPACING_UNIT }}>
|
||||
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
||||
<div style={{ width: 24, height: 24, position: "relative" }}>
|
||||
<Lottie
|
||||
animationData={flameAnimation}
|
||||
loop
|
||||
autoplay
|
||||
style={{
|
||||
width: 40,
|
||||
top: -10,
|
||||
left: -5,
|
||||
position: "absolute",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{t(currentCatalogueCategory)}
|
||||
</h2>
|
||||
|
||||
<section className={styles.cards}>
|
||||
{isLoading
|
||||
|
||||
@@ -155,6 +155,7 @@ export const listItemImage = style({
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "4px",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const listItemDetails = style({
|
||||
|
||||
@@ -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<FormValues>({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const [validationResult, setValidationResult] = useState<{
|
||||
name: string;
|
||||
downloadCount: number;
|
||||
} | null>(null);
|
||||
const [validationResult, setValidationResult] =
|
||||
useState<DownloadSourceValidationResult | null>(null);
|
||||
|
||||
const { sourceUrl } = useContext(settingsContext);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormValues) => {
|
||||
setIsLoading(true);
|
||||
const existingDownloadSource = await downloadSourcesTable
|
||||
.where({ url: values.url })
|
||||
.first();
|
||||
|
||||
try {
|
||||
const result = await window.electron.validateDownloadSource(values.url);
|
||||
setValidationResult(result);
|
||||
if (existingDownloadSource) {
|
||||
setError("url", {
|
||||
type: "server",
|
||||
message: t("source_already_exists"),
|
||||
});
|
||||
|
||||
setUrl(values.url);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (
|
||||
error.message.endsWith("Source with the same url already exists")
|
||||
) {
|
||||
setError("url", {
|
||||
type: "server",
|
||||
message: t("source_already_exists"),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadSourcesWorker.postMessage([
|
||||
"VALIDATE_DOWNLOAD_SOURCE",
|
||||
values.url,
|
||||
]);
|
||||
|
||||
const channel = new BroadcastChannel(
|
||||
`download_sources:validate:${values.url}`
|
||||
);
|
||||
|
||||
channel.onmessage = (
|
||||
event: MessageEvent<DownloadSourceValidationResult>
|
||||
) => {
|
||||
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")}
|
||||
</Button>
|
||||
|
||||
@@ -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<DownloadSource[]>([]);
|
||||
const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
|
||||
useState(false);
|
||||
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
|
||||
useState(false);
|
||||
|
||||
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const { indexRepacks } = useContext(repacksContext);
|
||||
|
||||
const getDownloadSources = async () => {
|
||||
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() {
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
disabled={!downloadSources.length || isSyncingDownloadSources}
|
||||
disabled={
|
||||
!downloadSources.length ||
|
||||
isSyncingDownloadSources ||
|
||||
isRemovingDownloadSource
|
||||
}
|
||||
onClick={syncDownloadSources}
|
||||
>
|
||||
<SyncIcon />
|
||||
@@ -99,6 +124,7 @@ export function SettingsDownloadSources() {
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||
disabled={isSyncingDownloadSources}
|
||||
>
|
||||
<PlusCircleIcon />
|
||||
{t("add_download_source")}
|
||||
@@ -148,6 +174,7 @@ export function SettingsDownloadSources() {
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||
disabled={isRemovingDownloadSource}
|
||||
>
|
||||
<NoEntryIcon />
|
||||
{t("remove_download_source")}
|
||||
|
||||
@@ -81,7 +81,7 @@ export const UserFriendModal = ({
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<p>Seu código de amigo: </p>
|
||||
<p>{t("your_friend_code")}</p>
|
||||
<button
|
||||
className={styles.friendCodeButton}
|
||||
onClick={copyToClipboard}
|
||||
|
||||
165
src/renderer/src/workers/download-sources.worker.ts
Normal file
165
src/renderer/src/workers/download-sources.worker.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
|
||||
import { z } from "zod";
|
||||
import axios, { AxiosError, AxiosHeaders } from "axios";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
|
||||
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),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
type Payload =
|
||||
| ["IMPORT_DOWNLOAD_SOURCE", string]
|
||||
| ["DELETE_DOWNLOAD_SOURCE", number]
|
||||
| ["VALIDATE_DOWNLOAD_SOURCE", string]
|
||||
| ["SYNC_DOWNLOAD_SOURCES", string];
|
||||
|
||||
self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||
const [type, data] = event.data;
|
||||
|
||||
if (type === "VALIDATE_DOWNLOAD_SOURCE") {
|
||||
const response =
|
||||
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||
|
||||
const { name } = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const channel = new BroadcastChannel(`download_sources:validate:${data}`);
|
||||
|
||||
channel.postMessage({
|
||||
name,
|
||||
etag: response.headers["etag"],
|
||||
downloadCount: response.data.downloads.length,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "DELETE_DOWNLOAD_SOURCE") {
|
||||
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||
await repacksTable.where({ downloadSourceId: data }).delete();
|
||||
await downloadSourcesTable.where({ id: data }).delete();
|
||||
});
|
||||
|
||||
const channel = new BroadcastChannel(`download_sources:delete:${data}`);
|
||||
|
||||
channel.postMessage(true);
|
||||
}
|
||||
|
||||
if (type === "IMPORT_DOWNLOAD_SOURCE") {
|
||||
const response =
|
||||
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||
|
||||
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||
const now = new Date();
|
||||
|
||||
const id = await downloadSourcesTable.add({
|
||||
url: data,
|
||||
name: response.data.name,
|
||||
etag: response.headers["etag"],
|
||||
status: DownloadSourceStatus.UpToDate,
|
||||
downloadCount: response.data.downloads.length,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const downloadSource = await downloadSourcesTable.get(id);
|
||||
|
||||
const repacks = response.data.downloads.map((download) => ({
|
||||
title: download.title,
|
||||
uris: download.uris,
|
||||
fileSize: download.fileSize,
|
||||
repacker: response.data.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSourceId: downloadSource!.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
|
||||
await repacksTable.bulkAdd(repacks);
|
||||
});
|
||||
|
||||
const channel = new BroadcastChannel(`download_sources:import:${data}`);
|
||||
channel.postMessage(true);
|
||||
}
|
||||
|
||||
if (type === "SYNC_DOWNLOAD_SOURCES") {
|
||||
const channel = new BroadcastChannel(`download_sources:sync:${data}`);
|
||||
let newRepacksCount = 0;
|
||||
|
||||
try {
|
||||
const downloadSources = await downloadSourcesTable.toArray();
|
||||
const existingRepacks = await repacksTable.toArray();
|
||||
|
||||
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);
|
||||
|
||||
await db.transaction(
|
||||
"rw",
|
||||
repacksTable,
|
||||
downloadSourcesTable,
|
||||
async () => {
|
||||
await downloadSourcesTable.update(downloadSource.id, {
|
||||
etag: response.headers["etag"],
|
||||
downloadCount: source.downloads.length,
|
||||
status: DownloadSourceStatus.UpToDate,
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const repacks = source.downloads
|
||||
.filter(
|
||||
(download) =>
|
||||
!existingRepacks.some(
|
||||
(repack) => repack.title === download.title
|
||||
)
|
||||
)
|
||||
.map((download) => ({
|
||||
title: download.title,
|
||||
uris: download.uris,
|
||||
fileSize: download.fileSize,
|
||||
repacker: source.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSourceId: downloadSource.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
|
||||
newRepacksCount += repacks.length;
|
||||
|
||||
await repacksTable.bulkAdd(repacks);
|
||||
}
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
const isNotModified = (err as AxiosError).response?.status === 304;
|
||||
|
||||
await downloadSourcesTable.update(downloadSource.id, {
|
||||
status: isNotModified
|
||||
? DownloadSourceStatus.UpToDate
|
||||
: DownloadSourceStatus.Errored,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
channel.postMessage(newRepacksCount);
|
||||
} catch (err) {
|
||||
channel.postMessage(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
5
src/renderer/src/workers/index.ts
Normal file
5
src/renderer/src/workers/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import RepacksWorker from "./repacks.worker?worker";
|
||||
import DownloadSourcesWorker from "./download-sources.worker?worker";
|
||||
|
||||
export const repacksWorker = new RepacksWorker();
|
||||
export const downloadSourcesWorker = new DownloadSourcesWorker();
|
||||
50
src/renderer/src/workers/repacks.worker.ts
Normal file
50
src/renderer/src/workers/repacks.worker.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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<GameRepack, "uris"> {
|
||||
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;
|
||||
|
||||
return {
|
||||
...repack,
|
||||
uris: [...repack.uris, repack.magnet].filter(Boolean),
|
||||
};
|
||||
});
|
||||
|
||||
const channel = new BroadcastChannel(`repacks:search:${requestId}`);
|
||||
|
||||
channel.postMessage(results);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -170,6 +168,10 @@ export interface UserBlocks {
|
||||
blocks: UserFriend[];
|
||||
}
|
||||
|
||||
export interface FriendRequestSync {
|
||||
friendRequestCount: number;
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@@ -190,27 +192,51 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
|
||||
sessionDurationInSeconds: number;
|
||||
}
|
||||
|
||||
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
|
||||
export interface UserDetails {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
profileVisibility: ProfileVisibility;
|
||||
bio: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
profileVisibility: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
totalPlayTimeInSeconds: number;
|
||||
profileVisibility: ProfileVisibility;
|
||||
libraryGames: UserGame[];
|
||||
recentGames: UserGame[];
|
||||
friends: UserFriend[];
|
||||
totalFriends: number;
|
||||
relation: UserRelation | null;
|
||||
currentGame: UserProfileCurrentGame | null;
|
||||
bio: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileRequest {
|
||||
displayName?: string;
|
||||
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
profileVisibility?: ProfileVisibility;
|
||||
profileImageUrl?: string | null;
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export interface DownloadSourceDownload {
|
||||
title: string;
|
||||
uris: string[];
|
||||
uploadDate: string;
|
||||
fileSize: string;
|
||||
}
|
||||
|
||||
export interface DownloadSourceValidationResult {
|
||||
name: string;
|
||||
etag: string;
|
||||
downloadCount: number;
|
||||
}
|
||||
|
||||
export interface DownloadSource {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user