Merge branch 'main' into linux-install

This commit is contained in:
Daniel Freitas 2024-05-24 13:54:41 -03:00 committed by GitHub
commit ee17b5106c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
127 changed files with 2494 additions and 2399 deletions

View File

@ -1,5 +1,4 @@
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
MAIN_VITE_ONLINEFIX_USERNAME=YOUR_USERNAME
MAIN_VITE_ONLINEFIX_PASSWORD=YOUR_PASSWORD
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
RENDERER_VITE_SENTRY_DSN=YOUR_SENTRY_DSN

View File

@ -1,29 +0,0 @@
---
name: Bug report
about: Create a report to help us improve. Write in English, please
title: "[BUG]"
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
- OS: [Windows 11/Linux Distro/Steam Deck]
- Hydra Version:
- Additional information and context of your problem:

58
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Bug Report
description: Create a report to help us improve. Write in English, please.
title: "[BUG] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thank you for creating a bug report to help us improve!
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: bug-reproduce
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior. For example, "1. Go to '...', 2. Click on '...', 3. See error"
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: input
id: OS
attributes:
label: Operating System
description: Which operating system are you using (e.g., Windows 11/Linux Distro/Steam Deck)?
validations:
required: true
- type: input
id: hydra-version
attributes:
label: Hydra Version
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

View File

@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for Hydra. Write in English, please
title: "[REQUEST]"
labels: enhancement
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,37 @@
name: Feature Request
description: Request a new feature.
title: "[REQUEST] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to suggest a new feature!
- type: textarea
id: problem-related
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

View File

@ -1,8 +1,6 @@
name: Build
on:
push:
branches: main
on: [pull_request]
jobs:
build:
@ -39,10 +37,6 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
env:
MAIN_VITE_STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -51,10 +45,6 @@ jobs:
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
MAIN_VITE_STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -64,7 +54,7 @@ jobs:
with:
name: Build-${{ matrix.os }}
path: |
dist/*.exe
dist/win-unpacked/**
dist/*.zip
dist/*.dmg
dist/*.deb
@ -72,11 +62,3 @@ jobs:
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
- name: VirusTotal Scan
uses: crazy-max/ghaction-virustotal@v4
if: matrix.os == 'windows-latest'
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
./dist/*.exe

View File

@ -26,3 +26,6 @@ jobs:
- name: Lint
run: yarn lint
- name: Format check
run: yarn format-check

70
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: Release
on:
push:
branches: main
jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.11.1
- name: Install dependencies
run: yarn
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build with cx_Freeze
run: python torrent-client/setup.py build
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
env:
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release
uses: softprops/action-gh-release@v1
with:
draft: true
files: |
dist/*.exe
dist/*.zip
dist/*.dmg
dist/*.AppImage
dist/*.snap
dist/*.deb
dist/*.rpm
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@ -10,5 +10,3 @@ out
.env
.vite
# Sentry Config File
.env.sentry-build-plugin

View File

@ -1,2 +1 @@
yarn lint
yarn typecheck
yarn format

View File

@ -1 +1,2 @@
yarn format
yarn lint
yarn typecheck

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict = true

View File

@ -1,5 +1,6 @@
out
dist
seeds
pnpm-lock.yaml
LICENSE.md
tsconfig.json

View File

@ -119,7 +119,7 @@ yarn
### Усталёўка Python 3.9
Упэўніцеся, што ў вас усталяваны Python 3.9 на вашым кампутары. Вы можаце загрузіць і ўсталяваць яго з [python.org](https://www.python.org/downloads/release/python-3919/).
Упэўніцеся, што ў вас усталяваны Python 3.9 на вашым кампутары. Вы можаце загрузіць і ўсталяваць яго з [python.org](https://www.python.org/downloads/release/python-3913/).
### Усталёўка залежнасцяў Python

182
README.es.md Normal file
View File

@ -0,0 +1,182 @@
<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 es un launcher de juegos con su propio cliente de bittorrent y gestor propio de repacks.</strong>
</p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Tabla de Contenidos
- [Acerca de](#acerca-de)
- [Características](#caracteristicas)
- [Instalación](#Instalacion)
- [Contribuir](#contribuir)
- [Únete a nuestro Telegram](#unete-a-nuestro-telegram)
- [Haz un fork y clona tu repositorio](#haz-un-fork-y-clona-tu-repositorio)
- [Maneras en las que puedes contribuir](#maneras-en-las-que-puedes-contribuir)
- [Estructura del proyecto](#estructura-del-proyecto)
- [Compilar desde el código fuente](#compilar-desde-el-código-fuente)
- [Instalar Node.js](#instalar-nodejs)
- [Instalar Yarn](#instalar-yarn)
- [Instalar Dependencias de Node](#instalar-dependencias-de-node)
- [Instalar Python 3.9](#instalar-python-39)
- [Instalar Dependencias de Python](#Instalar-dependencias-de-python)
- [Variables del Entorno](#variables-del-entorno)
- [Ejecución](#ejecucion)
- [Compilación](#compilacion)
- [Compilar el cliente de bittorrent](#compilar-el-cliente-de-bittorrent)
- [Compilar la aplicación Electron](#compilar-la-aplicacion-electron)
- [Colaboradores](#colaboradores)
## Acerca de
**Hydra** es un **Launcher de Juegos** con su propio **Cliente Bittorrent** y **autogestor de Repacks**.
<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 ...
## Instalacion
Sigue los pasos de abajo para instalar:
1. Descarga la última versión de Hydra desde la página de [Releases](https://github.com/hydralauncher/hydra/releases/latest).
- Descarga solo el .exe si quieres instalar Hydra en Windows.
- Descarga el .deb o .rpm o .zip si quieres instalar Hydra en Linux. (Depende de tu distro de Linux)
2. Ejecuta el archivo descargado.
3. ¡Disfruta de Hydra!
## <a name="contribuir"> Contribuir
### <a name="unete-a-nuestro-telegram"></a> Unete a nuestro Telegram
Puedes unirte a nuestra conversación y discusiones en nuestro canal de [Telegram](https://t.me/hydralauncher).
### Haz un fork y clona tu repositorio
1. Rea;iza un fork del repositorio [(Haz click acá para hacer un fork ahora)](https://github.com/hydralauncher/hydra/fork)
2. Clona el código forkeado `git clone https://github.com/tu_nombredeusuario/hydra`
3. Crea una nueva rama
4. Sube tus commits
5. Envía nuevas solicitudes de pull
### Maneras en las que puedes contribuir
- Traducción: Queremos que Hydra esté disponible para todas las personas que sean posible. Siéntete libre de ayudarnos a traducirlo a nuevos lenguajes o actualizar y mejorar las ya disponibles en Hydra.
- Código: Hydra está hecho con Typescript, Electron y un poquito de Python. Si quieres contribuir, ¡únete a nuestro [Telegram](https://t.me/hydralauncher)!
### Estructura del proyecto
- torrent-client: Usamos libtorrent, una librería de Python que se encarga de manejar las descargas torrent
- src/renderer: El UI de la aplicación
- src/main: El resto de la lógica va acá.
## Compilar desde el código fuente
### Instalar Node.js
Asegúrate que tienes Node.js instalado en tú máquina. Si no es así, puedes descargarlo e instalarlo desde [nodejs.org](https://nodejs.org/).
### Instalar Yarn
Yarn es un gestor de paquetes para Node.js. Si no tienes aún instalado Yarn todavía, puedes hacerlo siguiendo las instrucciones en [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Instalar Dependencias de Node
Dirígete hasta el directorio del proyecto e instala las dependencias de Node usando Yarn:
```bash
cd hydra
yarn
```
### Instalar Python 3.9
Asegúrate que tienes Python 3.9 instalado en tu máquina. Puedes descargarlo e instalarlo desde [python.org](https://www.python.org/downloads/release/python-3913/).
### Instalar Dependencias de Python
Instala las dependencias de Python requeridas usando pip:
```bash
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`.
## Ejecucion
Una vez que tengas todas las cosas listas, puedes ejecutar el siguiente comando para así iniciar el proceso de Electron y el cliente de bittorrent:
```bash
yarn dev
```
## Compilacion
### Compilar el cliente de bittorrent
Crea el cliente bittorrent usando este comando:
```bash
python torrent-client/setup.py build
```
### Compilar la aplicacion Electron
Crea la aplicación de Electron usando este comando:
En Windows:
```bash
yarn build:win
```
En Linux:
```bash
yarn build:linux
```
## Colaboradores
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Licencia
Hydra está licenciado bajo la [MIT License](LICENSE).

View File

@ -13,11 +13,11 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -119,7 +119,7 @@ yarn
### Install Python 3.9
Ensure you have Python 3.9 installed on your machine. You can download and install it from [python.org](https://www.python.org/downloads/release/python-3919/).
Ensure you have Python 3.9 installed on your machine. You can download and install it from [python.org](https://www.python.org/downloads/release/python-3913/).
### Install Python Dependencies

185
README.pl.md Normal file
View File

@ -0,0 +1,185 @@
<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 - to program uruchamiający gry z własnym wbudowanym klientem bittorrent i samodzielnie zarządzanym repackagerem..</strong>
</p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Zawartość.
- [O nas](#o-nas)
- [Cechy.](#cechy)
- [Instalacja](#instalacja)
- [Dokonaj wpłaty](#dokonaj-wpłaty)
- [Dołącz do naszego kanału Telegram](#dołącz-do-naszego-kanału-telegram)
- [Rozwidlenie i sklonowanie repozytorium](#rozwidlenie-i-sklonowanie-repozytorium)
- [Jak możesz wnieść swój wkład](#jak-możesz-pomóc)
- [Struktura projektu](#struktura-projektu)
- [Utwórz kompilację z kodu źródłowego](#utwórz-kompilację-z-kodu-źródłowego)
- [Instalacja Node.js](#zainstaluj-nodejs)
- [Instalacja Yarn](#zainstaluj-yarn)
- [Instalacja Node zależności](#zainstaluj-zależności-node)
- [Instalacja Python 3.9](#zainstaluj-python-39)
- [Instalacja Python zależności](#zainstaluj-zależności-pythona)
- [Zmienne środowiskowe](#zmienne-środowiskowe)
- [Uruchomienie](#utwórz-kompilację-z-kodu-źródłowego)
- [Tworzenie kompilacji](#tworzenie-kompilacji)
- [Tworzenie klienta bittorrent](#zbuduj-klienta-bittorrent)
- [Tworzenie kompilacji aplikacji Electron](#tworzenie-aplikacji-electron)
- [Współtwórcy](#współtwórcy)
## O nas
**Hydra** - jest **programem uruchamiającym gry** z wbudowanym **klientem BitTorrent** i **samozarządzającym się repackagerem**.
<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 ...
## Instalacja
Aby zainstalować, wykonaj poniższe czynności:
1. Pobierz najnowszą wersję programu Hydra ze strony [Wydania](https://github.com/hydralauncher/hydra/releases/latest).
- Pobierz .exe tylko, jeśli chcesz zainstalować Hydrę w systemie Windows.
- Pobierz .deb lub .rpm lub .zip, jeśli chcesz zainstalować Hydrę w systemie Linux (zależy od dystrybucji systemu Linux).
2. Uruchom pobrany plik.
3. Ciesz się Hydrą!
## <a name="contributing"> Dokonaj wpłaty
### <a name="join-our-telegram"></a> Dołącz do naszego kanału Telegram
Skupiamy nasze dyskusje na naszym kanale [Telegram](https://t.me/hydralauncher).
1. Dołącz do naszego kanału
2. Przejdź do kanału ról i wybierz rolę Pracownik.
3. Wejdź na kanał dev, komunikuj się z nami i dziel się swoimi pomysłami.
### Rozwidlenie i sklonowanie repozytorium
1. Rozwidlenie repozytorium [(kliknij tutaj, aby rozwidlić teraz)](https://github.com/hydralauncher/hydra/fork)
2. Sklonuj swój rozwidlony kod `git clone https://github.com/your_username/hydra`.
3. Utwórz nowy brunch
4. Wypchnij swoje zatwierdzenia
5. Wyślij nowy Pull Request
### Jak możesz pomóc
- Tłumaczenie: Chcemy, aby Hydra była dostępna dla jak największej liczby osób. Zachęcamy do pomocy w tłumaczeniu na nowe języki lub aktualizowaniu i ulepszaniu tych, które są już dostępne na Hydrze.
- Kod: Hydra jest zbudowana przy użyciu Typescript, Electron i odrobiny Pythona. Jeśli chcesz wnieść swój wkład, dołącz do naszego kanału Telegram!
### Struktura projektu
- Klient torrent: Używamy libtorrent, biblioteki Pythona, do zarządzania pobieraniem torrentów.
- src/renderer: interfejs aplikacji
- src/main: cała logika jest tutaj.
## Utwórz kompilację z kodu źródłowego
### Zainstaluj Node.js
Upewnij się, że masz zainstalowany Node.js na swoim komputerze. Jeśli nie, pobierz i zainstaluj go ze strony [nodejs.org](https://nodejs.org/).
### Zainstaluj Yarn
Yarn to menedżer pakietów dla Node.js. Jeśli jeszcze nie zainstalowałeś Yarn, możesz to zrobić, postępując zgodnie z instrukcjami na stronie [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Zainstaluj zależności Node
Przejdź do katalogu projektu i zainstaluj zależności Node za pomocą Yarn:
```bash
cd hydra
yarn
```
### Zainstaluj Python 3.9
Upewnij się, że masz zainstalowany Python 3.9 na swoim komputerze. Można go pobrać i zainstalować ze strony [python.org](https://www.python.org/downloads/release/python-3913/).
### Zainstaluj zależności Pythona
Zainstaluj niezbędne zależności Pythona za pomocą pip:
```bash
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`.
## Run
Po skonfigurowaniu wszystkiego można uruchomić następujące polecenie, aby uruchomić zarówno proces Electron, jak i klienta bittorrent:
```bash
yarn dev
```
## Tworzenie kompilacji
### Zbuduj klienta bittorrent
Zbuduj klienta bittorrent za pomocą tego poleceniaи:
```bash
python torrent-client/setup.py build
```
### Tworzenie aplikacji Electron
Zbuduj aplikację Electron za pomocą tego polecenia:
W systemie Windows:
```bash
yarn build:win
```
W systemie Linux:
```bash
yarn build:linux
```
## Współtwórcy
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## License
Hydra posiada licencję [MIT License](LICENSE).

View File

@ -13,13 +13,11 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
@ -120,7 +118,7 @@ yarn
### <a name="install-python-39"></a> Instale Python 3.9
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3919/).
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).
### <a name="install-python-dependencies"></a> Instale Python Dependencies

View File

@ -13,11 +13,11 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -119,7 +119,7 @@ yarn
### Установка Python 3.9
Убедитесь, что у вас установлен Python 3.9 на вашем компьютере. Вы можете загрузить и установить его с [python.org](https://www.python.org/downloads/release/python-3919/).
Убедитесь, что у вас установлен Python 3.9 на вашем компьютере. Вы можете загрузить и установить его с [python.org](https://www.python.org/downloads/release/python-3913/).
### Установка зависимостей Python

View File

@ -13,11 +13,11 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -123,7 +123,7 @@ yarn
### Встановіть Python 3.9
Переконайтеся, що на вашому комп'ютері встановлено Python 3.9. Ви можете завантажити та встановити його з [python.org](https://www.python.org/downloads/release/python-3919/).
Переконайтеся, що на вашому комп'ютері встановлено Python 3.9. Ви можете завантажити та встановити його з [python.org](https://www.python.org/downloads/release/python-3913/).
### Встановіть Python залежності

View File

@ -6,6 +6,7 @@ extraResources:
- hydra-download-manager
- hydra.db
- fastlist.exe
- seeds
files:
- "!**/.vscode/*"
- "!src/*"
@ -46,5 +47,7 @@ appImage:
npmRebuild: false
publish:
provider: github
owner: hydralauncher
repo: hydra
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

View File

@ -7,17 +7,10 @@ import {
} from "electron-vite";
import react from "@vitejs/plugin-react";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import svgr from "vite-plugin-svgr";
export default defineConfig(({ mode }) => {
loadEnv(mode);
const sentryPlugin = sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: "hydra-launcher",
project: "hydra-launcher",
});
return {
main: {
build: {
@ -34,7 +27,7 @@ export default defineConfig(({ mode }) => {
"@shared": resolve("src/shared"),
},
},
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin],
plugins: [externalizeDepsPlugin(), swcPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
@ -50,7 +43,7 @@ export default defineConfig(({ mode }) => {
"@shared": resolve("src/shared"),
},
},
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin],
plugins: [svgr(), react(), vanillaExtractPlugin()],
},
};
});

BIN
hydra.db

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "hydra",
"version": "1.1.1",
"name": "hydralauncher",
"version": "1.2.4",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@ -10,8 +10,13 @@
"url": "https://github.com/hydralauncher/hydra.git"
},
"type": "module",
"engines": {
"npm": "please-use-yarn",
"yarn": ">= 1.19.1"
},
"scripts": {
"format": "prettier --write .",
"format-check": "prettier --check .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
@ -24,7 +29,8 @@
"build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux",
"prepare": "husky"
"prepare": "husky",
"typeorm:migration-create": "yarn typeorm migration:create"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
@ -33,9 +39,6 @@
"@fontsource/fira-sans": "^5.0.20",
"@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3",
"@sentry/electron": "^4.23.0",
"@sentry/react": "^7.111.0",
"@sentry/vite-plugin": "^2.16.1",
"@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/recipes": "^0.5.2",
"auto-launch": "^5.0.6",
@ -47,6 +50,7 @@
"color.js": "^1.2.0",
"date-fns": "^3.6.0",
"easydl": "^1.1.1",
"electron-updater": "^6.1.8",
"fetch-cookie": "^3.0.1",
"flexsearch": "^0.7.43",
"i18next": "^23.11.2",

1
seeds/steam-games.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -55,62 +55,62 @@
"remove_from_list": "إزالة",
"space_left_on_disk": "{{space}} متبقية على القرص",
"eta": "الوقت المتبقي {{eta}}",
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
"checking_files": "جاري التحقق من الملفات...",
"filter": "تصفية حزم إعادة التجميع",
"requirements": "متطلبات النظام",
"minimum": "الحد الأدنى",
"recommended": "موصى به",
"no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات",
"no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها",
"paused_progress": "{{progress}} (متوقف)",
"release_date": "تم الإصدار في {{date}}",
"publisher": "نشر بواسطة {{publisher}}",
"copy_link_to_clipboard": "نسخ الرابط",
"copied_link_to_clipboard": "تم نسخ الرابط",
"hours": "ساعات",
"minutes": "دقائق",
"amount_hours": "{{amount}} ساعات",
"amount_minutes": "{{amount}} دقائق",
"accuracy": "دقة {{accuracy}}%",
"add_to_library": "إضافة إلى المكتبة",
"remove_from_library": "إزالة من المكتبة",
"no_downloads": "لا توجد تنزيلات متاحة",
"play_time": "تم اللعب لمدة {{amount}}",
"last_time_played": "آخر مرة لعبت {{period}}",
"not_played_yet": "لم تلعب {{title}} بعد",
"next_suggestion": "الاقتراح التالي",
"play": "لعب",
"deleting": "جاري حذف المثبت...",
"close": "إغلاق",
"playing_now": "قيد التشغيل الآن",
"change": "تغيير",
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها",
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى الإعدادات",
"download_now": "تنزيل الآن",
"installation_instructions": "إرشادات التثبيت",
"installation_instructions_description": "هناك خطوات إضافية مطلوبة لتثبيت هذه اللعبة",
"online_fix_instruction": "تتطلب ألعاب OnlineFix كلمة مرور لاستخراجها. عند الحاجة، استخدم كلمة المرور التالية:",
"dodi_installation_instruction": "عند فتح مثبت DODI، اضغط على مفتاح التشغيل لأعلى <0 /> لبدء عملية التثبيت:",
"dont_show_it_again": "لا تعرضها مرة أخرى",
"copy_to_clipboard": "نسخ",
"copied_to_clipboard": "تم النسخ",
"got_it": "حسنأ",
"no_shop_details": "لم يتم استرداد تفاصيل المتجر.",
"download_options": "خيارات التنزيل",
"download_path": "مسار التنزيل",
"previous_screenshot": "لقطة الشاشة السابقة",
"next_screenshot": "لقطة الشاشة التالية",
"screenshot": "لقطة شاشة {{number}}",
"open_screenshot": "افتح لقطة الشاشة {{number}}"
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
"checking_files": "جاري التحقق من الملفات...",
"filter": "تصفية حزم إعادة التجميع",
"requirements": "متطلبات النظام",
"minimum": "الحد الأدنى",
"recommended": "موصى به",
"no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات",
"no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها",
"paused_progress": "{{progress}} (متوقف)",
"release_date": "تم الإصدار في {{date}}",
"publisher": "نشر بواسطة {{publisher}}",
"copy_link_to_clipboard": "نسخ الرابط",
"copied_link_to_clipboard": "تم نسخ الرابط",
"hours": "ساعات",
"minutes": "دقائق",
"amount_hours": "{{amount}} ساعات",
"amount_minutes": "{{amount}} دقائق",
"accuracy": "دقة {{accuracy}}%",
"add_to_library": "إضافة إلى المكتبة",
"remove_from_library": "إزالة من المكتبة",
"no_downloads": "لا توجد تنزيلات متاحة",
"play_time": "تم اللعب لمدة {{amount}}",
"last_time_played": "آخر مرة لعبت {{period}}",
"not_played_yet": "لم تلعب {{title}} بعد",
"next_suggestion": "الاقتراح التالي",
"play": "لعب",
"deleting": "جاري حذف المثبت...",
"close": "إغلاق",
"playing_now": "قيد التشغيل الآن",
"change": "تغيير",
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها",
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى الإعدادات",
"download_now": "تنزيل الآن",
"installation_instructions": "إرشادات التثبيت",
"installation_instructions_description": "هناك خطوات إضافية مطلوبة لتثبيت هذه اللعبة",
"online_fix_instruction": "تتطلب ألعاب OnlineFix كلمة مرور لاستخراجها. عند الحاجة، استخدم كلمة المرور التالية:",
"dodi_installation_instruction": "عند فتح مثبت DODI، اضغط على مفتاح التشغيل لأعلى <0 /> لبدء عملية التثبيت:",
"dont_show_it_again": "لا تعرضها مرة أخرى",
"copy_to_clipboard": "نسخ",
"copied_to_clipboard": "تم النسخ",
"got_it": "حسنأ",
"no_shop_details": "لم يتم استرداد تفاصيل المتجر.",
"download_options": "خيارات التنزيل",
"download_path": "مسار التنزيل",
"previous_screenshot": "لقطة الشاشة السابقة",
"next_screenshot": "لقطة الشاشة التالية",
"screenshot": "لقطة شاشة {{number}}",
"open_screenshot": "افتح لقطة الشاشة {{number}}"
},
"activation": {
"title": "تفعيل هايدرا",
"installation_id": "معرف التثبيت:",
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
"message": "إذا كنت لا تعرف أين تسأل عن هذا ، فلا يجب أن يكون لديك هذا.",
"title": "تفعيل هايدرا",
"installation_id": "معرف التثبيت:",
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
"message": "إذا كنت لا تعرف أين تسأل عن هذا ، فلا يجب أن يكون لديك هذا.",
"activate": "تفعيل",
"loading": "جار التحميل…"
"loading": "جار التحميل…"
},
"downloads": {
"resume": "استئناف",
@ -146,7 +146,7 @@
"telemetry": "القياس عن بعد",
"telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر",
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal Debrid ",
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
"general": "عام",
"behavior": "السلوك",
@ -177,4 +177,4 @@
"modal": {
"close": "زر إغلاق"
}
}
}

View File

@ -176,5 +176,11 @@
},
"modal": {
"close": "Close button"
},
"splash": {
"downloading_version": "Downloading version {{version}}",
"searching_updates": "Searching for updates",
"update_found": "Update {{version}} found",
"restarting_and_applying": "Restarting and applying update"
}
}

View File

@ -24,12 +24,12 @@
"github": "Contribuye en GitHub"
},
"header": {
"search": "Buscar",
"search": "Buscar juegos",
"home": "Inicio",
"catalogue": "Catálogo",
"downloads": "Descargas",
"search_results": "Resultados de búsqueda",
"settings": "Ajustes",
"home": "Inicio"
"settings": "Ajustes"
},
"bottom_panel": {
"no_downloads_in_progress": "Sin descargas en progreso",
@ -37,12 +37,17 @@
"checking_files": "Analizando archivos de {{title}} - ({{percentage}} completado)",
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Siguiente página",
"previous_page": "Pagina anterior"
},
"game_details": {
"open_download_options": "Ver opciones de descargas",
"download_options_zero": "No hay opciones de descargas disponibles",
"download_options_one": "{{count}} opción de descarga",
"download_options_other": "{{count}} opciones de descargas",
"updated_at": "Actualizado el {{updated_at}}",
"install": "Instalar",
"resume": "Continuar",
"pause": "Pausa",
"cancel": "Cancelar",
@ -52,7 +57,7 @@
"eta": "Finalizando en {{eta}}",
"downloading_metadata": "Descargando metadatos…",
"checking_files": "Analizando archivos…",
"filter": "Filtrar repacks",
"filter": "Buscar repacks",
"requirements": "Requisitos del Sistema",
"minimum": "Mínimos",
"recommended": "Recomendados",
@ -71,20 +76,17 @@
"add_to_library": "Agregar a la biblioteca",
"remove_from_library": "Eliminar de la biblioteca",
"no_downloads": "No hay descargas disponibles",
"next_suggestion": "Siguiente sugerencia",
"play_time": "Jugado por {{amount}}",
"install": "Instalar",
"play": "Jugar",
"not_played_yet": "Aún no has jugado a {{title}}",
"close": "Cerrar",
"deleting": "Eliminando instalador…",
"playing_now": "Jugando ahora",
"last_time_played": "Jugado por última vez {{period}}",
"got_it": "Entendido",
"not_played_yet": "Aún no has jugado a {{title}}",
"next_suggestion": "Siguiente sugerencia",
"play": "Jugar",
"deleting": "Eliminando instalador…",
"close": "Cerrar",
"playing_now": "Jugando ahora",
"change": "Cambiar",
"repacks_modal_description": "Selecciona el repack que quieres descargar",
"downloads_path": "Ruta de descarga",
"select_folder_hint": "Para cambiar la carpeta predeterminada, accede a",
"select_folder_hint": "Para cambiar la carpeta predeterminada, ve a <0>Ajustes</0>",
"download_now": "Descargar ahora",
"installation_instructions": "Instrucciones de instalación",
"installation_instructions_description": "Se requieren de pasos adicionales para instalar este juego",
@ -92,7 +94,15 @@
"dodi_installation_instruction": "Cuando abras el instalador de DODI, presiona la tecla hacia arriba del teclado <0 /> para iniciar el proceso de instalación:",
"dont_show_it_again": "No mostrar de nuevo",
"copy_to_clipboard": "Copiar",
"copied_to_clipboard": "Copiado"
"copied_to_clipboard": "Copiado",
"got_it": "Entendido",
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
"download_options": "Opciones de descarga",
"download_path": "Ruta de descarga",
"previous_screenshot": "Anterior captura",
"next_screenshot": "Siguiente captura",
"screenshot": "Captura {{number}}",
"open_screenshot": "Abrir captura {{number}}"
},
"activation": {
"title": "Activar Hydra",
@ -118,12 +128,14 @@
"downloading_metadata": "Descargando metadatos…",
"checking_files": "Verificando archivos…",
"starting_download": "Iniciando descarga…",
"remove_from_list": "Eliminar",
"delete": "Eliminar instalador",
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
"delete_modal_title": "¿Estás seguro?",
"deleting": "Eliminando instalador…",
"install": "Instalar"
"delete": "Eliminar instalador",
"remove_from_list": "Eliminar",
"delete_modal_title": "¿Estás seguro?",
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
"install": "Instalar",
"real_debrid": "Real Debrid",
"torrent": "Torrent"
},
"settings": {
"downloads_path": "Ruta de descarga",
@ -132,7 +144,16 @@
"enable_download_notifications": "Cuando se completa una descarga",
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
"telemetry": "Telemetría",
"telemetry_description": "Habilitar recopilación de datos de manera anónima"
"telemetry_description": "Habilitar recopilación de datos de manera anónima",
"real_debrid_api_token_label": "Token API de Real Debrid",
"quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema",
"launch_with_system": "Iniciar Hydra al inicio del sistema",
"general": "General",
"behavior": "Otros",
"enable_real_debrid": "Activar Real Debrid",
"real_debrid": "Real Debrid",
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.",
"save_changes": "Guardar cambios"
},
"notifications": {
"download_complete": "Descarga completada",
@ -156,8 +177,10 @@
"modal": {
"close": "Botón de cierre"
},
"catalogue": {
"next_page": "Siguiente página",
"previous_page": "Pagina anterior"
"splash": {
"downloading_version": "Descargando versión {{version}}",
"searching_updates": "Buscando actualizaciones",
"update_found": "Actualización {{version}} encontrada",
"restarting_and_applying": "Reiniciando y aplicando actualización"
}
}

View File

@ -15,7 +15,7 @@
"checking_files": "{{title}} ({{percentage}} - 파일 검사 중…)",
"paused": "{{title}} (일시 정지됨)",
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
"filter": "필터 라이브러리",
"filter": "라이브러리 정렬",
"follow_us": "공식 SNS",
"home": "홈",
"discord": "공식 디스코드",
@ -54,10 +54,10 @@
"remove": "제거",
"remove_from_list": "목록에서 제거",
"space_left_on_disk": "여유 저장 용량 {{space}} 남음",
"eta": "료까지 {{eta}}",
"eta": "료까지 {{eta}}",
"downloading_metadata": "메타데이터 다운로드 중…",
"checking_files": "파일 검사 중…",
"filter": "리팩들을 다음과 같이 걸러내기",
"filter": "리팩들을 다음과 같이 정렬하기",
"requirements": "시스템 사양",
"minimum": "최저 사양",
"recommended": "권장 사양",
@ -91,8 +91,8 @@
"download_now": "지금 다운로드",
"installation_instructions": "설치 방법",
"installation_instructions_description": "이 게임을 설치하기 위해서는 추가적인 단계가 필요합니다",
"online_fix_instruction": "OnlineFix 게임들은 추출 시 암호가 필요합니다. 비밀번호를 물을 때 다음을 암호로 사용하기:",
"dodi_installation_instruction": "DODI 인스톨러를 열었다면 키보드의 위 방향키를 눌러 설치를 시작하세요:",
"online_fix_instruction": "OnlineFix 게임들은 압축 해제 시 암호가 필요합니다. 비밀번호를 물을 때 다음을 암호로 사용하기:",
"dodi_installation_instruction": "DODI 인스톨러를 실행했다면 키보드의 위 방향키를 눌러 설치를 시작하세요:",
"dont_show_it_again": "다시 보지 않기",
"copy_to_clipboard": "복사하기",
"copied_to_clipboard": "복사됨",

View File

@ -17,7 +17,11 @@
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
"filter": "Filtruj biblioteke",
"follow_us": "Śledź nas",
"home": "Główna"
"home": "Główna",
"discord": "Dołącz nasz Discord",
"telegram": "Dołącz nasz Telegram",
"x": "Śledź na X",
"github": "Przyczyń się na GitHub"
},
"header": {
"search": "Szukaj",
@ -66,6 +70,8 @@
"copied_link_to_clipboard": "Skopiowano łącze",
"hours": "godzin",
"minutes": "minut",
"amount_hours": "{{amount}} godzin",
"amount_minutes": "{{amount}} minut",
"accuracy": "{{accuracy}}% dokładność",
"add_to_library": "Dodaj do biblioteki",
"remove_from_library": "Usuń z biblioteki",
@ -80,9 +86,23 @@
"playing_now": "Granie teraz",
"change": "Zmień",
"repacks_modal_description": "Wybierz repack, który chcesz pobrać",
"downloads_path": "Ścieżka pobierania",
"select_folder_hint": "Aby zmienić domyślny folder, przejdź do",
"download_now": "Pobierz teraz"
"download_now": "Pobierz teraz",
"installation_instructions": "Instrukcja instalacji",
"installation_instructions_description": "Do zainstalowania tej gry wymagane są dodatkowe kroki",
"online_fix_instruction": "Gry OnlineFix wymagają hasła do wyodrębnienia. W razie potrzeby użyj następującego hasła:",
"dodi_installation_instruction": "Po otwarciu instalatora DODI naciśnij klawisz <0 /> w górę, aby rozpocząć proces instalacji:",
"dont_show_it_again": "Nie pokazuj tego ponownie",
"copy_to_clipboard": "Skopiuj",
"copied_to_clipboard": "Skopiowano",
"got_it": "Rozumiem",
"no_shop_details": "Nie udało się pobrać danych sklepu.",
"download_options": "Opcje pobierania",
"download_path": "Ścieżka pobierania",
"previous_screenshot": "Poprzedni zrzut ekranu",
"next_screenshot": "Następny zrzut ekranu",
"screenshot": "Zrzut ekranu {{number}}",
"open_screenshot": "Otwórz zrzut ekranu {{number}}"
},
"activation": {
"title": "Aktywuj Hydra",
@ -113,7 +133,9 @@
"remove_from_list": "Usuń",
"delete_modal_title": "Czy na pewno?",
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
"install": "Instaluj"
"install": "Instaluj",
"real_debrid": "Real Debrid",
"torrent": "Torrent"
},
"settings": {
"downloads_path": "Ścieżka pobierania",
@ -122,7 +144,16 @@
"enable_download_notifications": "Gdy pobieranie zostanie zakończone",
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
"telemetry": "Telemetria",
"telemetry_description": "Włącz anonimowe statystyki użycia"
"telemetry_description": "Włącz anonimowe statystyki użycia",
"real_debrid_api_token_label": "Real Debrid API token",
"quit_app_instead_hiding": "Zamknij Hydr zamiast minimalizować do zasobnika",
"launch_with_system": "Uruchom Hydra przy starcie systemu",
"general": "Ogólne",
"behavior": "Zachowania",
"enable_real_debrid": "Włącz Real Debrid",
"real_debrid": "Real Debrid",
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>.",
"save_changes": "Zapisz zmiany"
},
"notifications": {
"download_complete": "Pobieranie zakończone",
@ -142,5 +173,8 @@
"title": "Programy nie są zainstalowane",
"description": "Pliki wykonywalne Wine lub Lutris nie zostały znalezione na twoim systemie",
"instructions": "Sprawdź prawidłowy sposób instalacji dowolnego z nich w swojej dystrybucji Linuksa, aby gra działała normalnie"
},
"modal": {
"close": "Zamknij"
}
}

View File

@ -176,5 +176,11 @@
},
"modal": {
"close": "Botão de fechar"
},
"splash": {
"downloading_version": "Baixando versão {{version}}",
"searching_updates": "Buscando atualizações",
"update_found": "Versão {{version}} encontrada",
"restarting_and_applying": "Reiniciando e aplicando atualização"
}
}

View File

@ -152,7 +152,7 @@
"behavior": "Поведение",
"enable_real_debrid": "Включить Real Debrid",
"real_debrid": "Real Debrid",
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь/0>.",
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>.",
"save_changes": "Сохранить изменения"
},
"notifications": {

View File

@ -150,7 +150,7 @@
"launch_with_system": "随系统启动时运行应用程序",
"enable_real_debrid": "启用 Real Debrid",
"real_debrid": "Real Debrid",
"real_debrid_api_token_hint": "您可以将API密钥填入<0>这里</0>.",
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
"save_changes": "保存更改"
},
"notifications": {

View File

@ -12,34 +12,20 @@ export const repackersOn1337x = [
export const repackers = [
...repackersOn1337x,
"Xatab",
"CPG",
"TinyRepacks",
"CPG",
"GOG",
"onlinefix",
] as const;
export const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const defaultDownloadsPath = app.getPath("downloads");
export const databasePath = path.join(
app.getPath("appData"),
app.getName(),
"hydra",
"hydra.db"
);
export const INSTALLATION_ID_LENGTH = 6;
export const ACTIVATION_KEY_MULTIPLIER = 7;
export const seedsPath = app.isPackaged
? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds");

View File

@ -1,33 +1,21 @@
import { DataSource } from "typeorm";
import {
Game,
GameShopCache,
Repack,
RepackerFriendlyName,
UserPreferences,
MigrationScript,
SteamGame,
} from "@main/entity";
import type { SqliteConnectionOptions } from "typeorm/driver/sqlite/SqliteConnectionOptions";
import { Game, GameShopCache, Repack, UserPreferences } from "@main/entity";
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
import { databasePath } from "./constants";
import migrations from "./migrations";
export const createDataSource = (options: Partial<SqliteConnectionOptions>) =>
export const createDataSource = (
options: Partial<BetterSqlite3ConnectionOptions>
) =>
new DataSource({
type: "better-sqlite3",
entities: [Game, Repack, UserPreferences, GameShopCache],
synchronize: true,
database: databasePath,
entities: [
Game,
Repack,
RepackerFriendlyName,
UserPreferences,
GameShopCache,
MigrationScript,
SteamGame,
],
...options,
});
export const dataSource = createDataSource({
synchronize: true,
migrations,
});

View File

@ -23,8 +23,8 @@ export class Game {
@Column("text")
title: string;
@Column("text")
iconUrl: string;
@Column("text", { nullable: true })
iconUrl: string | null;
@Column("text", { nullable: true })
folderName: string | null;

View File

@ -1,7 +1,4 @@
export * from "./game.entity";
export * from "./repack.entity";
export * from "./repacker-friendly-name.entity";
export * from "./user-preferences.entity";
export * from "./game-shop-cache.entity";
export * from "./migration-script.entity";
export * from "./steam-game.entity";

View File

@ -1,22 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity("migration_script")
export class MigrationScript {
@PrimaryGeneratedColumn()
id: number;
@Column("text", { unique: true })
version: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,25 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity("repacker_friendly_name")
export class RepackerFriendlyName {
@PrimaryGeneratedColumn()
id: number;
@Column("text", { unique: true })
name: string;
@Column("text")
friendlyName: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,10 +0,0 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity("steam_game")
export class SteamGame {
@PrimaryColumn()
id: number;
@Column()
name: string;
}

View File

@ -26,9 +26,6 @@ export class UserPreferences {
@Column("boolean", { default: false })
repackUpdatesNotificationsEnabled: boolean;
@Column("boolean", { default: true })
telemetryEnabled: boolean;
@Column("boolean", { default: false })
preferQuitInsteadOfHiding: boolean;

View File

@ -0,0 +1,48 @@
import { AppUpdaterEvents } from "@types";
import { registerEvent } from "../register-event";
import updater, { ProgressInfo, UpdateInfo } from "electron-updater";
import { WindowManager } from "@main/services";
import { app } from "electron";
const { autoUpdater } = updater;
const sendEvent = (event: AppUpdaterEvents) => {
WindowManager.splashWindow?.webContents.send("autoUpdaterEvent", event);
};
const mockValuesForDebug = async () => {
sendEvent({ type: "update-downloaded" });
};
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater
.addListener("error", () => {
sendEvent({ type: "error" });
})
.addListener("checking-for-update", () => {
sendEvent({ type: "checking-for-updates" });
})
.addListener("update-not-available", () => {
sendEvent({ type: "update-not-available" });
})
.addListener("update-available", (info: UpdateInfo) => {
sendEvent({ type: "update-available", info });
})
.addListener("update-downloaded", () => {
sendEvent({ type: "update-downloaded" });
})
.addListener("download-progress", (info: ProgressInfo) => {
sendEvent({ type: "download-progress", info });
})
.addListener("update-cancelled", () => {
sendEvent({ type: "update-cancelled" });
});
if (app.isPackaged) {
autoUpdater.checkForUpdates();
} else {
await mockValuesForDebug();
}
};
registerEvent("checkForUpdates", checkForUpdates);

View File

@ -0,0 +1,12 @@
import { WindowManager } from "@main/services";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
const { autoUpdater } = updater;
const continueToMainWindow = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
};
registerEvent("continueToMainWindow", continueToMainWindow);

View File

@ -0,0 +1,17 @@
import { app } from "electron";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
import { WindowManager } from "@main/services";
const { autoUpdater } = updater;
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
if (app.isPackaged) {
autoUpdater.quitAndInstall(true, true);
} else {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
}
};
registerEvent("restartAndInstallUpdate", restartAndInstallUpdate);

View File

@ -92,7 +92,4 @@ const getRecentlyAddedCatalogue = async (
return results.slice(0, resultSize);
};
registerEvent(getCatalogue, {
name: "getCatalogue",
memoize: true,
});
registerEvent("getCatalogue", getCatalogue);

View File

@ -1,9 +1,10 @@
import { gameShopCacheRepository, steamGameRepository } from "@main/repository";
import { gameShopCacheRepository } from "@main/repository";
import { getSteamAppDetails } from "@main/services";
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
import { registerEvent } from "../register-event";
import { stateManager } from "@main/state-manager";
const getLocalizedSteamAppDetails = (
objectID: string,
@ -13,10 +14,11 @@ const getLocalizedSteamAppDetails = (
return getSteamAppDetails(objectID, language);
}
return Promise.all([
steamGameRepository.findOne({ where: { id: Number(objectID) } }),
getSteamAppDetails(objectID, language),
]).then(([steamGame, localizedAppDetails]) => {
return getSteamAppDetails(objectID, language).then((localizedAppDetails) => {
const steamGame = stateManager
.getValue("steamGames")
.find((game) => game.id === Number(objectID));
if (steamGame && localizedAppDetails) {
return {
...localizedAppDetails,
@ -72,7 +74,4 @@ const getGameShopDetails = async (
throw new Error("Not implemented");
};
registerEvent(getGameShopDetails, {
name: "getGameShopDetails",
memoize: true,
});
registerEvent("getGameShopDetails", getGameShopDetails);

View File

@ -36,7 +36,4 @@ const getGames = async (
return { results, cursor: i };
};
registerEvent(getGames, {
name: "getGames",
memoize: true,
});
registerEvent("getGames", getGames);

View File

@ -42,7 +42,4 @@ const getHowLongToBeat = async (
});
};
registerEvent(getHowLongToBeat, {
name: "getHowLongToBeat",
memoize: true,
});
registerEvent("getHowLongToBeat", getHowLongToBeat);

View File

@ -36,6 +36,4 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
return state.games[state.index];
};
registerEvent(getRandomGame, {
name: "getRandomGame",
});
registerEvent("getRandomGame", getRandomGame);

View File

@ -8,7 +8,4 @@ const searchGameRepacks = (
return searchRepacks(query);
};
registerEvent(searchGameRepacks, {
name: "searchGameRepacks",
memoize: true,
});
registerEvent("searchGameRepacks", searchGameRepacks);

View File

@ -9,7 +9,4 @@ const searchGamesEvent = async (
return searchGames({ query, take: 12 });
};
registerEvent(searchGamesEvent, {
name: "searchGames",
memoize: true,
});
registerEvent("searchGames", searchGamesEvent);

View File

@ -7,6 +7,4 @@ const getDiskFreeSpace = async (
path: string
) => checkDiskSpace(path);
registerEvent(getDiskFreeSpace, {
name: "getDiskFreeSpace",
});
registerEvent("getDiskFreeSpace", getDiskFreeSpace);

View File

@ -14,7 +14,6 @@ import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
import "./library/get-repackers-friendly-names";
import "./library/open-game";
import "./library/open-game-installer";
import "./library/remove-game";
@ -28,6 +27,9 @@ import "./torrenting/start-game-download";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./autoupdater/continue-to-main-window";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View File

@ -3,8 +3,8 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getFileBase64 } from "@main/helpers";
import { getSteamGameIconUrl } from "@main/services";
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
import { stateManager } from "@main/state-manager";
const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
@ -27,21 +27,31 @@ const addGameToLibrary = async (
)
.then(async ({ affected }) => {
if (!affected) {
const iconUrl = await getFileBase64(
await getSteamGameIconUrl(objectID)
);
const steamGame = stateManager
.getValue("steamGames")
.find((game) => game.id === Number(objectID));
await gameRepository.insert({
title,
iconUrl,
objectID,
shop: gameShop,
executablePath,
});
const iconUrl = steamGame?.clientIcon
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
: null;
await gameRepository
.insert({
title,
iconUrl,
objectID,
shop: gameShop,
executablePath,
})
.then(() => {
if (iconUrl) {
getFileBase64(iconUrl).then((base64) =>
gameRepository.update({ objectID }, { iconUrl: base64 })
);
}
});
}
});
};
registerEvent(addGameToLibrary, {
name: "addGameToLibrary",
});
registerEvent("addGameToLibrary", addGameToLibrary);

View File

@ -36,6 +36,4 @@ const closeGame = async (
return false;
};
registerEvent(closeGame, {
name: "closeGame",
});
registerEvent("closeGame", closeGame);

View File

@ -47,6 +47,4 @@ const deleteGameFolder = async (
}
};
registerEvent(deleteGameFolder, {
name: "deleteGameFolder",
});
registerEvent("deleteGameFolder", deleteGameFolder);

View File

@ -16,6 +16,4 @@ const getGameByObjectID = async (
},
});
registerEvent(getGameByObjectID, {
name: "getGameByObjectID",
});
registerEvent("getGameByObjectID", getGameByObjectID);

View File

@ -28,6 +28,4 @@ const getLibrary = async () =>
)
);
registerEvent(getLibrary, {
name: "getLibrary",
});
registerEvent("getLibrary", getLibrary);

View File

@ -1,12 +0,0 @@
import { registerEvent } from "../register-event";
import { stateManager } from "@main/state-manager";
const getRepackersFriendlyNames = async () =>
stateManager.getValue("repackersFriendlyNames").reduce((prev, next) => {
return { ...prev, [next.name]: next.friendlyName };
}, {});
registerEvent(getRepackersFriendlyNames, {
name: "getRepackersFriendlyNames",
memoize: true,
});

View File

@ -54,6 +54,4 @@ const openGameInstaller = async (
return false;
};
registerEvent(openGameInstaller, {
name: "openGameInstaller",
});
registerEvent("openGameInstaller", openGameInstaller);

View File

@ -13,6 +13,4 @@ const openGame = async (
shell.openPath(executablePath);
};
registerEvent(openGame, {
name: "openGame",
});
registerEvent("openGame", openGame);

View File

@ -8,6 +8,4 @@ const removeGameFromLibrary = async (
gameRepository.update({ id: gameId }, { isDeleted: true });
};
registerEvent(removeGameFromLibrary, {
name: "removeGameFromLibrary",
});
registerEvent("removeGameFromLibrary", removeGameFromLibrary);

View File

@ -20,6 +20,4 @@ const removeGame = async (
);
};
registerEvent(removeGame, {
name: "removeGame",
});
registerEvent("removeGame", removeGame);

View File

@ -4,6 +4,4 @@ import { registerEvent } from "../register-event";
const openExternal = async (_event: Electron.IpcMainInvokeEvent, src: string) =>
shell.openExternal(src);
registerEvent(openExternal, {
name: "openExternal",
});
registerEvent("openExternal", openExternal);

View File

@ -13,6 +13,4 @@ const showOpenDialog = async (
throw new Error("Main window is not available");
};
registerEvent(showOpenDialog, {
name: "showOpenDialog",
});
registerEvent("showOpenDialog", showOpenDialog);

View File

@ -1,37 +1,11 @@
import { ipcMain } from "electron";
import { stateManager } from "@main/state-manager";
interface EventArgs {
name: string;
memoize?: boolean;
}
export const registerEvent = (
listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any,
{ name, memoize = false }: EventArgs
name: string,
listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any
) => {
ipcMain.handle(name, (event: Electron.IpcMainInvokeEvent, ...args) => {
const eventResults = stateManager.getValue("eventResults");
const keys = Array.from(eventResults.keys());
const key = [name, args] as [string, any[]];
const memoizationKey = keys.find(([memoizedEvent, memoizedArgs]) => {
const sameEvent = name === memoizedEvent;
const sameArgs = memoizedArgs.every((arg, index) => arg === args[index]);
return sameEvent && sameArgs;
});
if (memoizationKey) return eventResults.get(memoizationKey);
ipcMain.handle(name, async (event: Electron.IpcMainInvokeEvent, ...args) => {
return Promise.resolve(listener(event, ...args)).then((result) => {
if (memoize) {
eventResults.set(key, JSON.parse(JSON.stringify(result)));
stateManager.setValue("eventResults", eventResults);
}
if (!result) return result;
return JSON.parse(JSON.stringify(result));
});

View File

@ -50,6 +50,4 @@ const cancelGameDownload = async (
});
};
registerEvent(cancelGameDownload, {
name: "cancelGameDownload",
});
registerEvent("cancelGameDownload", cancelGameDownload);

View File

@ -27,6 +27,4 @@ const pauseGameDownload = async (
});
};
registerEvent(pauseGameDownload, {
name: "pauseGameDownload",
});
registerEvent("pauseGameDownload", pauseGameDownload);

View File

@ -46,6 +46,4 @@ const resumeGameDownload = async (
}
};
registerEvent(resumeGameDownload, {
name: "resumeGameDownload",
});
registerEvent("resumeGameDownload", resumeGameDownload);

View File

@ -1,4 +1,3 @@
import { getSteamGameIconUrl } from "@main/services";
import {
gameRepository,
repackRepository,
@ -8,10 +7,11 @@ import {
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getFileBase64 } from "@main/helpers";
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
import { In } from "typeorm";
import { DownloadManager } from "@main/services";
import { Downloader, GameStatus } from "@shared";
import { stateManager } from "@main/state-manager";
const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@ -76,18 +76,34 @@ const startGameDownload = async (
return game;
} else {
const iconUrl = await getFileBase64(await getSteamGameIconUrl(objectID));
const steamGame = stateManager
.getValue("steamGames")
.find((game) => game.id === Number(objectID));
const createdGame = await gameRepository.save({
title,
iconUrl,
objectID,
downloader,
shop: gameShop,
status: GameStatus.Downloading,
downloadPath,
repack: { id: repackId },
});
const iconUrl = steamGame?.clientIcon
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
: null;
const createdGame = await gameRepository
.save({
title,
iconUrl,
objectID,
downloader,
shop: gameShop,
status: GameStatus.Downloading,
downloadPath,
repack: { id: repackId },
})
.then((result) => {
if (iconUrl) {
getFileBase64(iconUrl).then((base64) =>
gameRepository.update({ objectID }, { iconUrl: base64 })
);
}
return result;
});
DownloadManager.downloadGame(createdGame.id);
@ -97,6 +113,4 @@ const startGameDownload = async (
}
};
registerEvent(startGameDownload, {
name: "startGameDownload",
});
registerEvent("startGameDownload", startGameDownload);

View File

@ -16,6 +16,4 @@ const autoLaunch = async (
}
};
registerEvent(autoLaunch, {
name: "autoLaunch",
});
registerEvent("autoLaunch", autoLaunch);

View File

@ -6,6 +6,4 @@ const getUserPreferences = async () =>
where: { id: 1 },
});
registerEvent(getUserPreferences, {
name: "getUserPreferences",
});
registerEvent("getUserPreferences", getUserPreferences);

View File

@ -21,6 +21,4 @@ const updateUserPreferences = async (
);
};
registerEvent(updateUserPreferences, {
name: "updateUserPreferences",
});
registerEvent("updateUserPreferences", updateUserPreferences);

View File

@ -13,7 +13,7 @@ import {
gogFormatter,
onlinefixFormatter,
} from "./formatters";
import { months, repackers } from "../constants";
import { repackers } from "../constants";
export const pipe =
<T>(...fns: ((arg: T) => any)[]) =>
@ -44,19 +44,6 @@ export const repackerFormatter: Record<
onlinefix: onlinefixFormatter,
};
export const formatUploadDate = (str: string) => {
const date = new Date();
const [month, day, year] = str.split(" ");
date.setMonth(months.indexOf(month.replace(".", "")));
date.setDate(Number(day.substring(0, 2)));
date.setFullYear(Number("20" + year.replace("'", "")));
date.setHours(0, 0, 0, 0);
return date;
};
export const getSteamAppAsset = (
category: "library" | "hero" | "logo" | "icon",
objectID: string,

View File

@ -1,29 +1,26 @@
import { app, BrowserWindow, net, protocol } from "electron";
import { init } from "@sentry/electron/main";
import updater from "electron-updater";
import i18n from "i18next";
import path from "node:path";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { resolveDatabaseUpdates, WindowManager } from "@main/services";
import { logger, resolveDatabaseUpdates, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source";
import * as resources from "@locales";
import { userPreferencesRepository } from "@main/repository";
const { autoUpdater } = updater;
autoUpdater.setFeedURL({
provider: "github",
owner: "hydralauncher",
repo: "hydra",
});
autoUpdater.logger = logger;
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
if (import.meta.env.MAIN_VITE_SENTRY_DSN) {
init({
dsn: import.meta.env.MAIN_VITE_SENTRY_DSN,
beforeSend: async (event) => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
if (userPreferences?.telemetryEnabled) return event;
return null;
},
});
}
app.commandLine.appendSwitch("--no-sandbox");
i18n.init({
resources,
@ -57,6 +54,8 @@ app.whenReady().then(() => {
);
dataSource.initialize().then(async () => {
await dataSource.runMigrations();
await resolveDatabaseUpdates();
await import("./main");
@ -65,7 +64,7 @@ app.whenReady().then(() => {
where: { id: 1 },
});
WindowManager.createMainWindow();
WindowManager.createSplashScreen();
WindowManager.createSystemTray(userPreferences?.language || "en");
});
});

View File

@ -1,8 +1,7 @@
import { stateManager } from "./state-manager";
import { repackers } from "./constants";
import { repackersOn1337x, seedsPath } from "./constants";
import {
getNewGOGGames,
getNewRepacksFromCPG,
getNewRepacksFromUser,
getNewRepacksFromXatab,
getNewRepacksFromOnlineFix,
@ -12,8 +11,6 @@ import {
import {
gameRepository,
repackRepository,
repackerFriendlyNameRepository,
steamGameRepository,
userPreferencesRepository,
} from "./repository";
import { TorrentDownloader } from "./services";
@ -22,12 +19,16 @@ import { Notification } from "electron";
import { t } from "i18next";
import { GameStatus } from "@shared";
import { In } from "typeorm";
import fs from "node:fs";
import path from "node:path";
import { RealDebridClient } from "./services/real-debrid";
import { orderBy } from "lodash-es";
import { SteamGame } from "@types";
startProcessWatcher();
const track1337xUsers = async (existingRepacks: Repack[]) => {
for (const repacker of repackers) {
for (const repacker of repackersOn1337x) {
await getNewRepacksFromUser(
repacker,
existingRepacks.filter((repack) => repack.repacker === repacker)
@ -39,19 +40,16 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
const existingRepacks = stateManager.getValue("repacks");
Promise.allSettled([
getNewGOGGames(
existingRepacks.filter((repack) => repack.repacker === "GOG")
),
track1337xUsers(existingRepacks),
getNewRepacksFromXatab(
existingRepacks.filter((repack) => repack.repacker === "Xatab")
),
getNewRepacksFromCPG(
existingRepacks.filter((repack) => repack.repacker === "CPG")
getNewGOGGames(
existingRepacks.filter((repack) => repack.repacker === "GOG")
),
getNewRepacksFromOnlineFix(
existingRepacks.filter((repack) => repack.repacker === "onlinefix")
),
track1337xUsers(existingRepacks),
]).then(() => {
repackRepository.count().then((count) => {
const total = count - stateManager.getValue("repacks").length;
@ -74,23 +72,18 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
};
const loadState = async (userPreferences: UserPreferences | null) => {
const [friendlyNames, repacks, steamGames] = await Promise.all([
repackerFriendlyNameRepository.find(),
repackRepository.find({
order: {
createdAt: "desc",
},
}),
steamGameRepository.find({
order: {
name: "asc",
},
}),
]);
const repacks = await repackRepository.find({
order: {
createdAt: "desc",
},
});
const steamGames = JSON.parse(
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
) as SteamGame[];
stateManager.setValue("repackersFriendlyNames", friendlyNames);
stateManager.setValue("repacks", repacks);
stateManager.setValue("steamGames", steamGames);
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
import("./events");

View File

@ -0,0 +1,78 @@
import { createDataSource } from "@main/data-source";
import { Repack } from "@main/entity";
import { app } from "electron";
import { chunk } from "lodash-es";
import path from "path";
import { In, MigrationInterface, QueryRunner, Table } from "typeorm";
export class FixRepackUploadDate1715900413313 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "repack_temp",
columns: [
{ name: "title", type: "varchar" },
{ name: "old_id", type: "int" },
],
}),
true
);
await queryRunner.query(
`INSERT INTO repack_temp (title, old_id) SELECT title, id FROM repack WHERE repacker IN ('onlinefix', 'Xatab');`
);
await queryRunner.query(
`DELETE FROM repack WHERE repacker IN ('onlinefix', 'Xatab');`
);
const updateDataSource = createDataSource({
database: app.isPackaged
? path.join(process.resourcesPath, "hydra.db")
: path.join(__dirname, "..", "..", "hydra.db"),
});
await updateDataSource.initialize();
const updateRepackRepository = updateDataSource.getRepository(Repack);
const updatedRepacks = await updateRepackRepository.find({
where: {
repacker: In(["onlinefix", "Xatab"]),
},
});
const chunks = chunk(
updatedRepacks.map((repack) => {
const { id: _, ...rest } = repack;
return rest;
}),
500
);
for (const chunk of chunks) {
await queryRunner.manager
.createQueryBuilder(Repack, "repack")
.insert()
.values(chunk)
.orIgnore()
.execute();
}
await queryRunner.query(
`UPDATE game
SET repackId = (
SELECT id
from repack LEFT JOIN repack_temp ON repack_temp.title = repack.title
WHERE repack_temp.old_id = game.repackId
)
WHERE EXISTS (select old_id from repack_temp WHERE old_id = game.repackId)`
);
await queryRunner.dropTable("repack_temp");
}
public async down(_: QueryRunner): Promise<void> {
return;
}
}

View File

@ -0,0 +1,3 @@
import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate";
export default [FixRepackUploadDate1715900413313];

View File

@ -1,27 +1,11 @@
import { dataSource } from "./data-source";
import {
Game,
GameShopCache,
Repack,
RepackerFriendlyName,
UserPreferences,
MigrationScript,
SteamGame,
} from "@main/entity";
import { Game, GameShopCache, Repack, UserPreferences } from "@main/entity";
export const gameRepository = dataSource.getRepository(Game);
export const repackRepository = dataSource.getRepository(Repack);
export const repackerFriendlyNameRepository =
dataSource.getRepository(RepackerFriendlyName);
export const userPreferencesRepository =
dataSource.getRepository(UserPreferences);
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
export const migrationScriptRepository =
dataSource.getRepository(MigrationScript);
export const steamGameRepository = dataSource.getRepository(SteamGame);

View File

@ -0,0 +1,40 @@
import path from "node:path";
import fs from "node:fs";
import { getSteamGameClientIcon, logger } from "@main/services";
import { chunk } from "lodash-es";
import { seedsPath } from "@main/constants";
import type { SteamGame } from "@types";
const steamGamesPath = path.join(seedsPath, "steam-games.json");
const steamGames = JSON.parse(
fs.readFileSync(steamGamesPath, "utf-8")
) as SteamGame[];
const chunks = chunk(steamGames, 1500);
for (const chunk of chunks) {
await Promise.all(
chunk.map(async (steamGame) => {
if (steamGame.clientIcon) return;
const index = steamGames.findIndex((game) => game.id === steamGame.id);
try {
const clientIcon = await getSteamGameClientIcon(String(steamGame.id));
steamGames[index].clientIcon = clientIcon;
logger.log("info", `Set ${steamGame.name} client icon`);
} catch (err) {
steamGames[index].clientIcon = null;
logger.log("info", `Could not set icon for ${steamGame.name}`);
}
})
);
fs.writeFileSync(steamGamesPath, JSON.stringify(steamGames));
logger.log("info", "Updated steam games");
}

View File

@ -1,7 +1,6 @@
import path from "node:path";
import cp from "node:child_process";
import fs from "node:fs";
import * as Sentry from "@sentry/electron/main";
import { app, dialog } from "electron";
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
@ -87,8 +86,6 @@ export class TorrentDownloader extends Downloader {
downloadSpeed: payload.downloadSpeed,
timeRemaining: payload.timeRemaining,
});
} catch (err) {
Sentry.captureException(err);
} finally {
await new Promise((resolve) => setTimeout(resolve, 100));
}

View File

@ -1,13 +1,39 @@
import { JSDOM } from "jsdom";
import { formatUploadDate } from "@main/helpers";
import { Repack } from "@main/entity";
import { requestWebPage, savePage } from "./helpers";
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const request1337x = async (path: string) =>
requestWebPage(`https://1337xx.to${path}`);
const formatUploadDate = (str: string) => {
const date = new Date();
const [month, day, year] = str.split(" ");
date.setMonth(months.indexOf(month.replace(".", "")));
date.setDate(Number(day.substring(0, 2)));
date.setFullYear(Number("20" + year.replace("'", "")));
date.setHours(0, 0, 0, 0);
return date;
};
/* TODO: $a will often be null */
const getTorrentDetails = async (path: string) => {
const response = await request1337x(path);

View File

@ -1,64 +0,0 @@
import { JSDOM } from "jsdom";
import { Repack } from "@main/entity";
import { requestWebPage, savePage } from "./helpers";
import { logger } from "../logger";
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
export const getNewRepacksFromCPG = async (
existingRepacks: Repack[] = [],
page = 1
): Promise<void> => {
const data = await requestWebPage(`https://cpgrepacks.site/page/${page}`);
const { window } = new JSDOM(data);
const repacks: QueryDeepPartialEntity<Repack>[] = [];
try {
Array.from(window.document.querySelectorAll(".post")).forEach(($post) => {
const $title = $post.querySelector(".entry-title")!;
const uploadDate = $post.querySelector("time")?.getAttribute("datetime");
const $downloadInfo = Array.from(
$post.querySelectorAll(".wp-block-heading")
).find(($heading) => $heading.textContent?.startsWith("Download"));
/* Side note: CPG often misspells "Magnet" as "Magent" */
const $magnet = Array.from($post.querySelectorAll("a")).find(
($a) =>
$a.textContent?.startsWith("Magnet") ||
$a.textContent?.startsWith("Magent")
);
const fileSize = ($downloadInfo?.textContent ?? "")
.split("Download link => ")
.at(1);
repacks.push({
title: $title.textContent!,
fileSize: fileSize ?? "N/A",
magnet: $magnet!.href,
repacker: "CPG",
page,
uploadDate: uploadDate ? new Date(uploadDate) : new Date(),
});
});
} catch (err: unknown) {
logger.error((err as Error).message, { method: "getNewRepacksFromCPG" });
}
const newRepacks = repacks.filter(
(repack) =>
!existingRepacks.some(
(existingRepack) => existingRepack.title === repack.title
)
);
if (!newRepacks.length) return;
await savePage(newRepacks);
return getNewRepacksFromCPG(existingRepacks, page + 1);
};

View File

@ -6,32 +6,57 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity
const virtualConsole = new VirtualConsole();
const getUploadDate = (document: Document) => {
const $modifiedTime = document.querySelector(
'[property="article:modified_time"]'
) as HTMLMetaElement;
if ($modifiedTime) return $modifiedTime.content;
const $publishedTime = document.querySelector(
'[property="article:published_time"]'
) as HTMLMetaElement;
return $publishedTime.content;
};
const getDownloadLink = (document: Document) => {
const $latestDownloadButton = document.querySelector(
".download-btn:not(.lightweight-accordion *)"
) as HTMLAnchorElement;
if ($latestDownloadButton) return $latestDownloadButton.href;
const $downloadButton = document.querySelector(
".download-btn"
) as HTMLAnchorElement;
if (!$downloadButton) return null;
return $downloadButton.href;
};
const getMagnet = (downloadLink: string) => {
if (downloadLink.startsWith("http")) {
const { searchParams } = new URL(downloadLink);
return Buffer.from(searchParams.get("url")!, "base64").toString("utf-8");
}
return downloadLink;
};
const getGOGGame = async (url: string) => {
const data = await requestWebPage(url);
const { window } = new JSDOM(data, { virtualConsole });
const $modifiedTime = window.document.querySelector(
'[property="article:modified_time"]'
) as HTMLMetaElement;
const downloadLink = getDownloadLink(window.document);
if (!downloadLink) return null;
const $em = window.document.querySelector(
"p:not(.lightweight-accordion *) em"
)!;
const $em = window.document.querySelector("p em");
if (!$em) return null;
const fileSize = $em.textContent!.split("Size: ").at(1);
const $downloadButton = window.document.querySelector(
".download-btn:not(.lightweight-accordion *)"
) as HTMLAnchorElement;
const { searchParams } = new URL($downloadButton.href);
const magnet = Buffer.from(searchParams.get("url")!, "base64").toString(
"utf-8"
);
return {
fileSize: fileSize ?? "N/A",
uploadDate: new Date($modifiedTime.content),
uploadDate: new Date(getUploadDate(window.document)),
repacker: "GOG",
magnet,
magnet: getMagnet(downloadLink),
page: 1,
};
};
@ -62,7 +87,7 @@ export const getNewGOGGames = async (existingRepacks: Repack[] = []) => {
if (!gameExists) {
const game = await getGOGGame(href);
repacks.push({ ...game, title });
if (game) repacks.push({ ...game, title });
}
}

View File

@ -1,3 +1,4 @@
import axios from "axios";
import UserAgent from "user-agents";
import type { Repack } from "@main/entity";
@ -13,12 +14,13 @@ export const savePage = async (repacks: QueryDeepPartialEntity<Repack>[]) =>
export const requestWebPage = async (url: string) => {
const userAgent = new UserAgent();
return fetch(url, {
method: "GET",
headers: {
"User-Agent": userAgent.toString(),
},
}).then((response) => response.text());
return axios
.get(url, {
headers: {
"User-Agent": userAgent.toString(),
},
})
.then((response) => response.data);
};
export const decodeNonUtf8Response = async (res: Response) => {

View File

@ -1,5 +1,4 @@
export * from "./1337x";
export * from "./xatab";
export * from "./cpg-repacks";
export * from "./gog";
export * from "./online-fix";

View File

@ -1,22 +1,21 @@
import { Repack } from "@main/entity";
import { decodeNonUtf8Response, savePage } from "./helpers";
import { logger } from "../logger";
import parseTorrent, {
toMagnetURI,
Instance as TorrentInstance,
} from "parse-torrent";
import { JSDOM } from "jsdom";
import { format, parse, sub } from "date-fns";
import { ru } from "date-fns/locale";
import createWorker from "@main/workers/torrent-parser.worker?nodeWorker";
import { toMagnetURI } from "parse-torrent";
const worker = createWorker({});
import { onlinefixFormatter } from "@main/helpers";
import makeFetchCookie from "fetch-cookie";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { formatBytes } from "@shared";
const ONLINE_FIX_URL = "https://online-fix.me/";
let totalPages = 1;
export const getNewRepacksFromOnlineFix = async (
existingRepacks: Repack[] = [],
page = 1,
@ -73,18 +72,16 @@ export const getNewRepacksFromOnlineFix = async (
const repacks: QueryDeepPartialEntity<Repack>[] = [];
const articles = Array.from(document.querySelectorAll(".news"));
const totalPages = Number(
document.querySelector("nav > a:nth-child(13)")?.textContent
);
if (page == 1) {
totalPages = Number(
document.querySelector("nav > a:nth-child(13)")?.textContent
);
}
try {
await Promise.all(
articles.map(async (article) => {
const gameText = article.querySelector("h2.title")?.textContent?.trim();
if (!gameText) return;
const gameName = onlinefixFormatter(gameText);
const gameLink = article.querySelector("a")?.getAttribute("href");
if (!gameLink) return;
@ -93,32 +90,6 @@ export const getNewRepacksFromOnlineFix = async (
);
const gameDocument = new JSDOM(gamePage).window.document;
const uploadDateText = gameDocument.querySelector("time")?.textContent;
if (!uploadDateText) return;
let decodedDateText = uploadDateText;
// "Вчера" means yesterday.
if (decodedDateText.includes("Вчера")) {
const yesterday = sub(new Date(), { days: 1 });
const formattedYesterday = format(yesterday, "d LLLL yyyy", {
locale: ru,
});
decodedDateText = decodedDateText.replace(
"Вчера", // "Change yesterday to the default expected date format"
formattedYesterday
);
}
const uploadDate = parse(
decodedDateText,
"d LLLL yyyy, HH:mm",
new Date(),
{
locale: ru,
}
);
const torrentButtons = Array.from(
gameDocument.querySelectorAll("a")
).filter((a) => a.textContent?.includes("Torrent"));
@ -139,27 +110,27 @@ export const getNewRepacksFromOnlineFix = async (
?.getAttribute("href");
const torrentFile = Buffer.from(
await http(`${torrentPrePage}/${torrentLink}`).then((res) =>
await http(`${torrentPrePage}${torrentLink}`).then((res) =>
res.arrayBuffer()
)
);
const torrent = parseTorrent(torrentFile) as TorrentInstance;
const magnetLink = toMagnetURI({
infoHash: torrent.infoHash,
worker.once("message", (torrent) => {
if (!torrent) return;
const { name, created } = torrent;
repacks.push({
fileSize: formatBytes(torrent.length ?? 0),
magnet: toMagnetURI(torrent),
page: 1,
repacker: "onlinefix",
title: name,
uploadDate: created,
});
});
const torrentSizeInBytes = torrent.length;
if (!torrentSizeInBytes) return;
repacks.push({
fileSize: formatBytes(torrentSizeInBytes),
magnet: magnetLink,
page: 1,
repacker: "onlinefix",
title: gameName,
uploadDate: uploadDate,
});
worker.postMessage(torrentFile);
})
);
} catch (err: unknown) {
@ -177,9 +148,10 @@ export const getNewRepacksFromOnlineFix = async (
);
if (!newRepacks.length) return;
if (page === totalPages) return;
await savePage(newRepacks);
if (page === totalPages) return;
return getNewRepacksFromOnlineFix(existingRepacks, page + 1, cookieJar);
};

View File

@ -9,8 +9,12 @@ import { toMagnetURI } from "parse-torrent";
import type { Instance } from "parse-torrent";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { formatBytes } from "@shared";
import { getFileBuffer } from "@main/helpers";
const worker = createWorker({});
worker.setMaxListeners(11);
let totalPages = 1;
const formatXatabDate = (str: string) => {
const date = new Date();
@ -27,7 +31,7 @@ const formatXatabDate = (str: string) => {
const getXatabRepack = (
url: string
): Promise<{ fileSize: string; magnet: string; uploadDate: Date }> => {
): Promise<{ fileSize: string; magnet: string; uploadDate: Date } | null> => {
return new Promise((resolve) => {
(async () => {
const data = await requestWebPage(url);
@ -40,15 +44,20 @@ const getXatabRepack = (
".download-torrent"
) as HTMLAnchorElement;
if (!$downloadButton) throw new Error("Download button not found");
if (!$downloadButton) return resolve(null);
worker.once("message", (torrent: Instance | null) => {
if (!torrent) return resolve(null);
worker.once("message", (torrent: Instance) => {
resolve({
fileSize: formatBytes(torrent.length ?? 0),
magnet: toMagnetURI(torrent),
uploadDate: formatXatabDate($uploadDate!.textContent!),
});
});
const buffer = await getFileBuffer($downloadButton.href);
worker.postMessage(buffer);
})();
});
};
@ -63,25 +72,37 @@ export const getNewRepacksFromXatab = async (
const repacks: QueryDeepPartialEntity<Repack>[] = [];
for (const $a of Array.from(
window.document.querySelectorAll(".entry__title a")
)) {
try {
const repack = await getXatabRepack(($a as HTMLAnchorElement).href);
repacks.push({
title: $a.textContent!,
repacker: "Xatab",
...repack,
page,
});
} catch (err: unknown) {
logger.error((err as Error).message, {
method: "getNewRepacksFromXatab",
});
}
if (page === 1) {
totalPages = Number(
window.document.querySelector(
"#bottom-nav > div.pagination > a:nth-child(12)"
)?.textContent
);
}
const repacksFromPage = Array.from(
window.document.querySelectorAll(".entry__title a")
).map(($a) => {
return getXatabRepack(($a as HTMLAnchorElement).href)
.then((repack) => {
if (repack) {
repacks.push({
title: $a.textContent!,
repacker: "Xatab",
...repack,
page,
});
}
})
.catch((err: unknown) => {
logger.error((err as Error).message, {
method: "getNewRepacksFromXatab",
});
});
});
await Promise.all(repacksFromPage);
const newRepacks = repacks.filter(
(repack) =>
!existingRepacks.some(
@ -93,5 +114,7 @@ export const getNewRepacksFromXatab = async (
await savePage(newRepacks);
if (page === totalPages) return;
return getNewRepacksFromXatab(existingRepacks, page + 1);
};

View File

@ -1,5 +1,4 @@
import axios from "axios";
import { getSteamAppAsset } from "@main/helpers";
export interface SteamGridResponse {
success: boolean;
@ -59,16 +58,11 @@ export const getSteamGridGameById = async (
return response.data;
};
export const getSteamGameIconUrl = async (objectID: string) => {
export const getSteamGameClientIcon = async (objectID: string) => {
const {
data: { id: steamGridGameId },
} = await getSteamGridData(objectID, "games", "steam");
const steamGridGame = await getSteamGridGameById(steamGridGameId);
return getSteamAppAsset(
"icon",
objectID,
steamGridGame.data.platforms.steam.metadata.clienticon
);
return steamGridGame.data.platforms.steam.metadata.clienticon;
};

View File

@ -3,107 +3,9 @@ import { app } from "electron";
import { chunk } from "lodash-es";
import { createDataSource, dataSource } from "@main/data-source";
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
import {
migrationScriptRepository,
repackRepository,
repackerFriendlyNameRepository,
steamGameRepository,
} from "@main/repository";
import { MigrationScript } from "@main/entity/migration-script.entity";
import { Like } from "typeorm";
const migrationScripts = {
/*
0.0.6 -> 0.0.7
Xatab repacks were previously created with an incorrect upload date.
This migration script will update the upload date of all Xatab repacks.
*/
"0.0.7": async (updateRepacks: Repack[]) => {
const VERSION = "0.0.7";
const migrationScript = await migrationScriptRepository.findOne({
where: {
version: VERSION,
},
});
if (!migrationScript) {
const xatabRepacks = updateRepacks.filter(
(repack) => repack.repacker === "Xatab"
);
await dataSource.transaction(async (transactionalEntityManager) => {
await Promise.all(
xatabRepacks.map((repack) =>
transactionalEntityManager.getRepository(Repack).update(
{
title: repack.title,
repacker: repack.repacker,
},
{
uploadDate: repack.uploadDate,
}
)
)
);
await transactionalEntityManager.getRepository(MigrationScript).insert({
version: VERSION,
});
});
}
},
/*
1.0.1 -> 1.1.0
A few torrents scraped from 1337x were previously created with an incorrect upload date.
*/
"1.1.0": async () => {
const VERSION = "1.1.0";
const migrationScript = await migrationScriptRepository.findOne({
where: {
version: VERSION,
},
});
if (!migrationScript) {
await dataSource.transaction(async (transactionalEntityManager) => {
const repacks = await transactionalEntityManager
.getRepository(Repack)
.find({
where: {
uploadDate: Like("1%"),
},
});
await Promise.all(
repacks.map(async (repack) => {
return transactionalEntityManager
.getRepository(Repack)
.update(
{ id: repack.id },
{ uploadDate: new Date(repack.uploadDate) }
);
})
);
await transactionalEntityManager.getRepository(MigrationScript).insert({
version: VERSION,
});
});
}
},
};
export const runMigrationScripts = async (updateRepacks: Repack[]) => {
return Promise.all(
Object.values(migrationScripts).map((migrationScript) => {
return migrationScript(updateRepacks);
})
);
};
import { createDataSource } from "@main/data-source";
import { Repack } from "@main/entity";
import { repackRepository } from "@main/repository";
export const resolveDatabaseUpdates = async () => {
const updateDataSource = createDataSource({
@ -114,25 +16,8 @@ export const resolveDatabaseUpdates = async () => {
return updateDataSource.initialize().then(async () => {
const updateRepackRepository = updateDataSource.getRepository(Repack);
const updateRepackerFriendlyNameRepository =
updateDataSource.getRepository(RepackerFriendlyName);
const updateSteamGameRepository = updateDataSource.getRepository(SteamGame);
const [updateRepacks, updateSteamGames, updateRepackerFriendlyNames] =
await Promise.all([
updateRepackRepository.find(),
updateSteamGameRepository.find(),
updateRepackerFriendlyNameRepository.find(),
]);
await runMigrationScripts(updateRepacks);
await repackerFriendlyNameRepository
.createQueryBuilder()
.insert()
.values(updateRepackerFriendlyNames)
.orIgnore()
.execute();
const updateRepacks = await updateRepackRepository.find();
const updateRepacksChunks = chunk(updateRepacks, 800);
@ -144,16 +29,5 @@ export const resolveDatabaseUpdates = async () => {
.orIgnore()
.execute();
}
const steamGamesChunks = chunk(updateSteamGames, 800);
for (const chunk of steamGamesChunks) {
await steamGameRepository
.createQueryBuilder()
.insert()
.values(chunk)
.orIgnore()
.execute();
}
});
};

View File

@ -1,13 +1,24 @@
import { BrowserWindow, Menu, Tray, app } from "electron";
import {
BrowserWindow,
Menu,
MenuItem,
MenuItemConstructorOptions,
Tray,
app,
shell,
} from "electron";
import { is } from "@electron-toolkit/utils";
import { t } from "i18next";
import path from "node:path";
import icon from "@resources/icon.png?asset";
import trayIcon from "@resources/tray-icon.png?asset";
import { userPreferencesRepository } from "@main/repository";
import { gameRepository, userPreferencesRepository } from "@main/repository";
import { IsNull, Not } from "typeorm";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
public static splashWindow: Electron.BrowserWindow | null = null;
public static isReadyToShowMainWindow = false;
private static loadURL(hash = "") {
// HMR for renderer base on electron-vite cli.
@ -26,13 +37,51 @@ export class WindowManager {
}
}
private static loadSplashURL() {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.splashWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/splash`
);
} else {
this.splashWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash: "splash",
}
);
}
}
public static createSplashScreen() {
if (this.splashWindow) return;
this.splashWindow = new BrowserWindow({
width: 380,
height: 380,
frame: false,
resizable: false,
backgroundColor: "#1c1c1c",
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.loadSplashURL();
this.splashWindow.removeMenu();
}
public static createMainWindow() {
// Create the browser window.
if (this.mainWindow || !this.isReadyToShowMainWindow) return;
this.mainWindow = new BrowserWindow({
width: 1200,
height: 720,
minWidth: 1024,
minHeight: 540,
backgroundColor: "#1c1c1c",
titleBarStyle: "hidden",
...(process.platform === "linux" ? { icon } : {}),
trafficLightPosition: { x: 16, y: 16 },
@ -66,6 +115,12 @@ export class WindowManager {
});
}
public static prepareMainWindowAndCloseSplash() {
this.isReadyToShowMainWindow = true;
this.splashWindow?.close();
this.createMainWindow();
}
public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow();
this.loadURL(hash);
@ -77,33 +132,66 @@ export class WindowManager {
public static createSystemTray(language: string) {
const tray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate([
{
label: t("open", {
ns: "system_tray",
lng: language,
}),
type: "normal",
click: () => {
if (this.mainWindow) {
this.mainWindow.show();
} else {
this.createMainWindow();
}
const updateSystemTray = async () => {
const games = await gameRepository.find({
where: {
isDeleted: false,
executablePath: Not(IsNull()),
lastTimePlayed: Not(IsNull()),
},
},
{
label: t("quit", {
ns: "system_tray",
lng: language,
}),
type: "normal",
click: () => app.quit(),
},
]);
take: 5,
order: {
updatedAt: "DESC",
},
});
const recentlyPlayedGames: Array<MenuItemConstructorOptions | MenuItem> =
games.map(({ title, executablePath }) => ({
label: title,
type: "normal",
click: async () => {
if (!executablePath) return;
shell.openPath(executablePath);
},
}));
const contextMenu = Menu.buildFromTemplate([
{
label: t("open", {
ns: "system_tray",
lng: language,
}),
type: "normal",
click: () => {
if (this.mainWindow) {
this.mainWindow.show();
} else {
this.createMainWindow();
}
},
},
{
type: "separator",
},
...recentlyPlayedGames,
{
type: "separator",
},
{
label: t("quit", {
ns: "system_tray",
lng: language,
}),
type: "normal",
click: () => app.quit(),
},
]);
return contextMenu;
};
tray.setToolTip("Hydra");
tray.setContextMenu(contextMenu);
if (process.platform === "win32" || process.platform === "linux") {
tray.addListener("click", () => {
@ -117,6 +205,11 @@ export class WindowManager {
this.createMainWindow();
});
tray.addListener("right-click", async () => {
const contextMenu = await updateSystemTray();
tray.popUpContextMenu(contextMenu);
});
}
}
}

View File

@ -1,17 +1,14 @@
import type { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
import type { Repack } from "@main/entity";
import type { SteamGame } from "@types";
interface State {
repacks: Repack[];
repackersFriendlyNames: RepackerFriendlyName[];
steamGames: SteamGame[];
eventResults: Map<[string, any[]], any>;
}
const initialState: State = {
repacks: [],
repackersFriendlyNames: [],
steamGames: [],
eventResults: new Map(),
};
export class StateManager {

View File

@ -4,7 +4,6 @@ interface ImportMetaEnv {
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
readonly MAIN_VITE_ONLINEFIX_USERNAME: string;
readonly MAIN_VITE_ONLINEFIX_PASSWORD: string;
readonly MAIN_VITE_SENTRY_DSN: string;
}
interface ImportMeta {

View File

@ -4,13 +4,11 @@ import parseTorrent from "parse-torrent";
const port = parentPort;
if (!port) throw new Error("IllegalState");
export const getFileBuffer = async (url: string) =>
fetch(url, { method: "GET" }).then((response) =>
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
);
port.on("message", async (url: string) => {
const buffer = await getFileBuffer(url);
const torrent = await parseTorrent(buffer);
port.postMessage(torrent);
port.on("message", async (buffer: Buffer) => {
try {
const torrent = await parseTorrent(buffer);
port.postMessage(torrent);
} catch (err) {
port.postMessage(null);
}
});

View File

@ -7,6 +7,7 @@ import type {
GameShop,
TorrentProgress,
UserPreferences,
AppUpdaterEvents,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
@ -76,8 +77,6 @@ contextBridge.exposeInMainWorld("electron", {
executablePath
),
getLibrary: () => ipcRenderer.invoke("getLibrary"),
getRepackersFriendlyNames: () =>
ipcRenderer.invoke("getRepackersFriendlyNames"),
openGameInstaller: (gameId: number) =>
ipcRenderer.invoke("openGameInstaller", gameId),
openGame: (gameId: number, executablePath: string) =>
@ -114,4 +113,21 @@ contextBridge.exposeInMainWorld("electron", {
showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke("showOpenDialog", options),
platform: process.platform,
/* Splash */
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
value: AppUpdaterEvents
) => cb(value);
ipcRenderer.on("autoUpdaterEvent", listener);
return () => {
ipcRenderer.removeListener("autoUpdaterEvent", listener);
};
},
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
continueToMainWindow: () => ipcRenderer.invoke("continueToMainWindow"),
});

View File

@ -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: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: 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' 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;"
/>
</head>
<body style="background-color: #1c1c1c">

View File

@ -12,12 +12,11 @@ import {
import * as styles from "./app.css";
import { themeClass } from "./theme.css";
import { useLocation, useNavigate } from "react-router-dom";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
setSearch,
clearSearch,
setUserPreferences,
setRepackersFriendlyNames,
toggleDraggingDisabled,
} from "@renderer/features";
import { GameStatusHelper } from "@shared";
@ -28,7 +27,7 @@ export interface AppProps {
children: React.ReactNode;
}
export function App({ children }: AppProps) {
export function App() {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary();
@ -45,14 +44,11 @@ export function App({ children }: AppProps) {
);
useEffect(() => {
Promise.all([
window.electron.getUserPreferences(),
window.electron.getRepackersFriendlyNames(),
updateLibrary(),
]).then(([preferences, repackersFriendlyNames]) => {
dispatch(setUserPreferences(preferences));
dispatch(setRepackersFriendlyNames(repackersFriendlyNames));
});
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
([preferences]) => {
dispatch(setUserPreferences(preferences));
}
);
}, [navigate, location.pathname, dispatch, updateLibrary]);
useEffect(() => {
@ -132,7 +128,7 @@ export function App({ children }: AppProps) {
/>
<section ref={contentRef} className={styles.content}>
{children}
<Outlet />
</section>
</article>
</main>

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -5,7 +5,6 @@ import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
import * as styles from "./game-card.css";
import { useAppSelector } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
export interface GameCardProps
@ -24,10 +23,6 @@ const shopIcon = {
export function GameCard({ game, ...props }: GameCardProps) {
const { t } = useTranslation("game_card");
const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value
);
const uniqueRepackers = Array.from(
new Set(game.repacks.map(({ repacker }) => repacker))
);
@ -47,7 +42,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
<ul className={styles.downloadOptions}>
{uniqueRepackers.map((repacker) => (
<li key={repacker} className={styles.downloadOption}>
<span>{repackersFriendlyNames[repacker]}</span>
<span>{repacker}</span>
</li>
))}
</ul>

View File

@ -42,7 +42,7 @@ export function Modal({
}, [onClose]);
const isTopMostModal = () => {
const openModals = document.querySelectorAll("[role=modal]");
const openModals = document.querySelectorAll("[role=dialog]");
return (
openModals.length &&

Some files were not shown because too many files have changed in this diff Show More